Repository: software-mansion/TypeGPU
Pull request #1311 (2025-05-28 11:40:27)

feat: Cast elements to correct type when creating vectors

aleksanderkatan: All those checks seem to have affected the performance a bit

<img width="1486" alt="image" src="https://github.com/user-attachments/assets/338207ce-946e-413a-8a35-0366960c4388" />

reczkok: > I'm really on the fence about this — I get that the consistency between JS and WGSL is nice, but is it really worth the performance hit (which, as we've established, is kind of important)?

Maybe we could leave this in and figure out a way to provide users with a very fast mass vector constructor
aleksanderkatan: I think we could modify the schema casts for scalar values, so that `f32` and `f16` are identity, `i32` is `Math.floor(n)`, `u32` is `Math.max(0, Math.floor(n))`, and `bool` is left as is (all plus the undefined check). This would probably increase the performance and leave the most important part of the cast in place. What do you think?
reczkok: I think we should keep the casting logic to stay compliant with WGSL's behavior. I’m actually curious how much of the performance hit comes from the casting itself versus the overhead of the function calls.
aleksanderkatan: @reczkok seems like most of the overhead is in Math functions and array writes.

Before any changes:
<img width="1487" alt="image" src="https://github.com/user-attachments/assets/bbf61f39-67e3-442e-b025-6407196d3947" />

When calling schema constructors, but all they do is return the value passed in:
<img width="1487" alt="image" src="https://github.com/user-attachments/assets/d1022ad4-6db6-4855-953f-f1d90be07602" />

When calling schema constructors, and casting all values properly:
<img width="1487" alt="image" src="https://github.com/user-attachments/assets/4780a07b-ba03-45b9-abc5-5cdf1db60bb7" />


aleksanderkatan: The benchmark results are now wildly inconsistent. For one test, I get 30 ops/s five times in a row, then I get 11 ops/s five times in a row...

I added the coercion to the vector element assignment (thgough `[0]` and through `.x`). I also removed the float value casts, and I simplified a bit the integer casts.

Patch:
diff --git a/apps/typegpu-docs/src/pages/benchmark/suites.ts b/apps/typegpu-docs/src/pages/benchmark/suites.ts
index eb2740c4..df99635b 100644
--- a/apps/typegpu-docs/src/pages/benchmark/suites.ts
+++ b/apps/typegpu-docs/src/pages/benchmark/suites.ts
@@ -6,6 +6,7 @@ import type { BenchParameterSet } from './parameter-set.ts';
 import { compiledWriteSuite } from './test-suites/compiled-write.ts';
 import { partialWriteSuite } from './test-suites/partial-write.ts';
 import { vectorCreationSuite } from './test-suites/vector-creation.ts';
+import { castSuite } from './test-suites/cast.ts';
 
 export type TestIdentifier = `${string}_${string}`;
 
@@ -41,6 +42,7 @@ export const unfilteredSuites: Record<string, Suite> = {
   'Partial write': partialWriteSuite,
   'Compiled write': compiledWriteSuite,
   'Vector creation': vectorCreationSuite,
+  Cast: castSuite,
 };
 
 export function getFilteredSuites(selectedTests: TestIdentifier[]) {
diff --git a/packages/typegpu/src/data/numeric.ts b/packages/typegpu/src/data/numeric.ts
index 6de4ee8f..3ffa5849 100644
--- a/packages/typegpu/src/data/numeric.ts
+++ b/packages/typegpu/src/data/numeric.ts
@@ -1,6 +1,6 @@
-import bin from 'typed-binary';
 import { createDualImpl } from '../shared/generators.ts';
 import { $internal } from '../shared/symbols.ts';
+import { snip } from './dataTypes.ts';
 import type {
   AbstractFloat,
   AbstractInt,
@@ -11,7 +11,6 @@ import type {
   U16,
   U32,
 } from './wgslTypes.ts';
-import { snip } from './dataTypes.ts';
 
 export const abstractInt = {
   [$internal]: true,
@@ -23,31 +22,53 @@ export const abstractFloat = {
   type: 'abstractFloat',
 } as AbstractFloat;
 
+const boolCast = createDualImpl(
+  // CPU implementation
+  (v?: number | boolean) => {
+    if (v === undefined) {
+      return false;
+    }
+    if (typeof v === 'boolean') {
+      return v;
+    }
+    return !!v;
+  },
+  // GPU implementation
+  (v) => snip(`bool(${v?.value ?? ''})`, bool),
+  'boolCast',
+);
+
 /**
  * A schema that represents a boolean value. (equivalent to `bool` in WGSL)
+ *
+ * Can also be called to cast a value to a bool in accordance with WGSL casting rules.
+ *
+ * @example
+ * const value = bool(); // false
+ * @example
+ * const value = bool(0); // false
+ * @example
+ * const value = bool(-0); // false
+ * @example
+ * const value = bool(21.37); // true
  */
-export const bool: Bool = {
-  [$internal]: true,
+export const bool: Bool = Object.assign(boolCast, {
   type: 'bool',
-} as Bool;
+}) as unknown as Bool;
 
 const u32Cast = createDualImpl(
   // CPU implementation
-  (v: number | boolean) => {
+  (v?: number | boolean) => {
+    if (v === undefined) {
+      return 0;
+    }
     if (typeof v === 'boolean') {
       return v ? 1 : 0;
     }
-    if (Number.isInteger(v)) {
-      if (v < 0 || v > 0xffffffff) {
-        console.warn(`u32 value ${v} overflowed`);
-      }
-      const value = v & 0xffffffff;
-      return value >>> 0;
-    }
-    return Math.max(0, Math.min(0xffffffff, Math.floor(v)));
+    return (v & 0xffffffff) >>> 0;
   },
   // GPU implementation
-  (v) => snip(`u32(${v.value})`, u32),
+  (v) => snip(`u32(${v?.value ?? ''})`, u32),
   'u32Cast',
 );
 
@@ -57,6 +78,10 @@ const u32Cast = createDualImpl(
  * Can also be called to cast a value to an u32 in accordance with WGSL casting rules.
  *
  * @example
+ * const value = u32(); // 0
+ * @example
+ * const value = u32(7); // 7
+ * @example
  * const value = u32(3.14); // 3
  * @example
  * const value = u32(-1); // 4294967295
@@ -69,25 +94,17 @@ export const u32: U32 = Object.assign(u32Cast, {
 
 const i32Cast = createDualImpl(
   // CPU implementation
-  (v: number | boolean) => {
+  (v?: number | boolean) => {
+    if (v === undefined) {
+      return 0;
+    }
     if (typeof v === 'boolean') {
       return v ? 1 : 0;
     }
-    if (Number.isInteger(v)) {
-      if (v < -0x80000000 || v > 0x7fffffff) {
-        console.warn(`i32 value ${v} overflowed`);
-      }
-      const value = v | 0;
-      return value & 0xffffffff;
-    }
-    // round towards zero
-    const value = v < 0 ? Math.ceil(v) : Math.floor(v);
-    return Math.max(-0x80000000, Math.min(0x7fffffff, value));
+    return v | 0;
   },
   // GPU implementation
-  (v) => {
-    return snip(`i32(${v.value})`, i32);
-  },
+  (v) => snip(`i32(${v?.value ?? ''})`, i32),
   'i32Cast',
 );
 
@@ -102,6 +119,8 @@ export const u16: U16 = {
  * Can also be called to cast a value to an i32 in accordance with WGSL casting rules.
  *
  * @example
+ * const value = i32(); // 0
+ * @example
  * const value = i32(3.14); // 3
  * @example
  * const value = i32(-3.9); // -3
@@ -114,16 +133,17 @@ export const i32: I32 = Object.assign(i32Cast, {
 
 const f32Cast = createDualImpl(
   // CPU implementation
-  (v: number | boolean) => {
+  (v?: number | boolean) => {
+    if (v === undefined) {
+      return 0;
+    }
     if (typeof v === 'boolean') {
       return v ? 1 : 0;
     }
-    const arr = new Float32Array(1);
-    arr[0] = v;
-    return arr[0];
+    return Math.fround(v);
   },
   // GPU implementation
-  (v) => snip(`f32(${v.value})`, f32),
+  (v) => snip(`f32(${v?.value ?? ''})`, f32),
   'f32Cast',
 );
 
@@ -133,25 +153,120 @@ const f32Cast = createDualImpl(
  * Can also be called to cast a value to an f32.
  *
  * @example
+ * const value = f32(); // 0
+ * @example
+ * const value = f32(1.23); // 1.23
+ * @example
  * const value = f32(true); // 1
  */
 export const f32: F32 = Object.assign(f32Cast, {
   type: 'f32',
 }) as unknown as F32;
 
+// helpers for floating point conversion
+const buf32 = new ArrayBuffer(4);
+const f32arr = new Float32Array(buf32);
+const u32arr = new Uint32Array(buf32);
+
+/**
+ * Convert a JavaScript number (treated as float32) to **binary16** bit pattern.
+ * @param x 32-bit floating-point value
+ * @returns 16-bit half-precision encoding (stored in a JS number)
+ */
+export function toHalfBits(x: number): number {
+  f32arr[0] = x; // Write value; shared buffer now contains raw bits.
+  const bits = u32arr[0] as number; // Read those bits as unsigned int.
+
+  // 1. Extract sign, exponent, and mantissa from the 32‑bit layout.
+  const sign = (bits >>> 31) & 0x1; // Bit 31 is the sign.
+  let exp = (bits >>> 23) & 0xff; // Bits 30‑23 form the biased exponent.
+  let mant = bits & 0x7fffff; // Bits 22‑0 are the significand.
+
+  // 2. Handle special values (NaN, ±∞) before re‑biasing.
+  if (exp === 0xff) {
+    // Preserve the quiet‑NaN bit if mant≠0; otherwise this is ±∞.
+    return (sign << 15) | 0x7c00 | (mant ? 0x0200 : 0);
+  }
+
+  // 3. Re‑bias the exponent from 127 → 15 (binary32 → binary16).
+  exp = exp - 127 + 15;
+
+  // 4. Underflow: exponent ≤ 0 yields sub‑normals or signed zero.
+  if (exp <= 0) {
+    // If we need to shift more than 10 places, the value rounds to ±0.
+    if (exp < -10) {
+      return sign << 15;
+    }
+
+    // Produce a sub‑normal: prepend the hidden 1, right‑shift, then round.
+    mant = (mant | 0x800000) >> (1 - exp);
+    mant = (mant + 0x1000) >> 13; // Round‑to‑nearest‑even at bit 10.
+    return (sign << 15) | mant;
+  }
+
+  // 5. Overflow: if the biased exponent is 31 (0x1f) or higher, the number
+  //    cannot be represented in half precision, so we return ±∞.
+  if (exp >= 0x1f) {
+    return (sign << 15) | 0x7c00; // ±∞
+  }
+
+  // 6. Normalised number: round mantissa and pack sign|exp|mant.
+  mant = mant + 0x1000; // Add rounding bias at bit 12.
+  if (mant & 0x800000) { // The carry propagated out of the top bit; mantissa overflowed.
+    mant = 0; // Rounded up to 1.0 × 2^(exp+1).
+    ++exp; // Increment exponent (may overflow to ±∞).
+    if (exp >= 0x1f) {
+      return (sign << 15) | 0x7c00;
+    }
+  }
+
+  return (sign << 15) | (exp << 10) | (mant >> 13);
+}
+
+/**
+ * Convert a **binary16** encoded bit pattern back to JavaScript number.
+ * @param h 16-bit half-precision bits
+ * @returns JavaScript number (64-bit float) with same numerical value
+ */
+export function fromHalfBits(h: number): number {
+  const sign = (h & 0x8000) ? -1 : 1; // Sign multiplier (preserves −0).
+  const exp = (h >> 10) & 0x1f; // 5‑bit exponent.
+  const mant = h & 0x03ff; // 10‑bit significand.
+
+  // 1. Zero and sub‑normals.
+  if (exp === 0) {
+    return mant ? sign * mant * 2 ** -24 : sign * 0;
+  }
+
+  // 2. Special cases (exp == 31).
+  if (exp === 0x1f) {
+    return mant
+      ? Number.NaN
+      : (sign === 1 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY);
+  }
+
+  // 3. Normalised numbers.
+  return sign * (1 + mant / 1024) * 2 ** (exp - 15);
+}
+
+function roundToF16(x: number): number {
+  return fromHalfBits(toHalfBits(x));
+}
+
 const f16Cast = createDualImpl(
   // CPU implementation
-  (v: number | boolean) => {
+  (v?: number | boolean) => {
+    if (v === undefined) {
+      return 0;
+    }
     if (typeof v === 'boolean') {
       return v ? 1 : 0;
     }
-    const arr = new ArrayBuffer(2);
-    bin.f16.write(new bin.BufferWriter(arr), v);
-    return bin.f16.read(new bin.BufferReader(arr));
+    return roundToF16(v);
   },
   // GPU implementation
   // TODO: make usage of f16() in GPU mode check for feature availability and throw if not available
-  (v) => snip(`f16(${v.value})`, f16),
+  (v) => snip(`f16(${v?.value ?? ''})`, f16),
   'f16Cast',
 );
 
@@ -161,6 +276,10 @@ const f16Cast = createDualImpl(
  * Can also be called to cast a value to an f16.
  *
  * @example
+ * const value = f16(); // 0
+ * @example
+ * const value = f32(1.23); // 1.23
+ * @example
  * const value = f16(true); // 1
  * @example
  * const value = f16(21877.5); // 21872
diff --git a/packages/typegpu/src/data/vectorImpl.ts b/packages/typegpu/src/data/vectorImpl.ts
index e888ff66..36ae39fc 100644
--- a/packages/typegpu/src/data/vectorImpl.ts
+++ b/packages/typegpu/src/data/vectorImpl.ts
@@ -1,11 +1,14 @@
 import { $internal } from '../shared/symbols.ts';
 import type { SelfResolvable } from '../types.ts';
+import { bool, f16, f32, i32, u32 } from './numeric.ts';
 import type { VecKind } from './wgslTypes.ts';
 
 // deno-fmt-ignore
 export abstract class VecBase<S> extends Array implements SelfResolvable {
   public readonly [$internal] = true;
   abstract get kind(): VecKind;
+  abstract getElementSchema(): (v?: S) => S;
+
 
   abstract get _Vec2(): new (
     x: S,
@@ -375,15 +378,30 @@ type Tuple4<S> = [S, S, S, S];
 
 abstract class Vec2<S> extends VecBase<S> implements Tuple2<S> {
   declare readonly length = 2;
-  abstract getDefaultValue(): S;
 
-  0: S;
-  1: S;
+  e0: S;
+  e1: S;
 
   constructor(x?: S, y?: S) {
     super(2);
-    this[0] = x ?? this.getDefaultValue();
-    this[1] = y ?? x ?? this.getDefaultValue();
+    this.e0 = this.getElementSchema()(x);
+    this.e1 = this.getElementSchema()(y ?? x);
+  }
+
+  get [0]() {
+    return this.e0;
+  }
+
+  get [1]() {
+    return this.e1;
+  }
+
+  set [0](value: S) {
+    this.e0 = this.getElementSchema()(value);
+  }
+
+  set [1](value: S) {
+    this.e1 = this.getElementSchema()(value);
   }
 
   get x() {
@@ -395,27 +413,50 @@ abstract class Vec2<S> extends VecBase<S> implements Tuple2<S> {
   }
 
   set x(value: S) {
-    this[0] = value;
+    this[0] = this.getElementSchema()(value);
   }
 
   set y(value: S) {
-    this[1] = value;
+    this[1] = this.getElementSchema()(value);
   }
 }
 
 abstract class Vec3<S> extends VecBase<S> implements Tuple3<S> {
   declare readonly length = 3;
-  abstract getDefaultValue(): S;
 
-  0: S;
-  1: S;
-  2: S;
+  e0: S;
+  e1: S;
+  e2: S;
 
   constructor(x?: S, y?: S, z?: S) {
     super(3);
-    this[0] = x ?? this.getDefaultValue();
-    this[1] = y ?? x ?? this.getDefaultValue();
-    this[2] = z ?? x ?? this.getDefaultValue();
+    this.e0 = this.getElementSchema()(x);
+    this.e1 = this.getElementSchema()(y ?? x);
+    this.e2 = this.getElementSchema()(z ?? x);
+  }
+
+  get [0]() {
+    return this.e0;
+  }
+
+  get [1]() {
+    return this.e1;
+  }
+
+  get [2]() {
+    return this.e2;
+  }
+
+  set [0](value: S) {
+    this.e0 = this.getElementSchema()(value);
+  }
+
+  set [1](value: S) {
+    this.e1 = this.getElementSchema()(value);
+  }
+
+  set [2](value: S) {
+    this.e2 = this.getElementSchema()(value);
   }
 
   get x() {
@@ -431,33 +472,64 @@ abstract class Vec3<S> extends VecBase<S> implements Tuple3<S> {
   }
 
   set x(value: S) {
-    this[0] = value;
+    this[0] = this.getElementSchema()(value);
   }
 
   set y(value: S) {
-    this[1] = value;
+    this[1] = this.getElementSchema()(value);
   }
 
   set z(value: S) {
-    this[2] = value;
+    this[2] = this.getElementSchema()(value);
   }
 }
 
 abstract class Vec4<S> extends VecBase<S> implements Tuple4<S> {
   declare readonly length = 4;
-  abstract getDefaultValue(): S;
 
-  0: S;
-  1: S;
-  2: S;
-  3: S;
+  e0: S;
+  e1: S;
+  e2: S;
+  e3: S;
 
   constructor(x?: S, y?: S, z?: S, w?: S) {
     super(4);
-    this[0] = x ?? this.getDefaultValue();
-    this[1] = y ?? x ?? this.getDefaultValue();
-    this[2] = z ?? x ?? this.getDefaultValue();
-    this[3] = w ?? x ?? this.getDefaultValue();
+    this.e0 = this.getElementSchema()(x);
+    this.e1 = this.getElementSchema()(y ?? x);
+    this.e2 = this.getElementSchema()(z ?? x);
+    this.e3 = this.getElementSchema()(w ?? x);
+  }
+
+  get [0]() {
+    return this.e0;
+  }
+
+  get [1]() {
+    return this.e1;
+  }
+
+  get [2]() {
+    return this.e2;
+  }
+
+  get [3]() {
+    return this.e3;
+  }
+
+  set [0](value: S) {
+    this.e0 = this.getElementSchema()(value);
+  }
+
+  set [1](value: S) {
+    this.e1 = this.getElementSchema()(value);
+  }
+
+  set [2](value: S) {
+    this.e2 = this.getElementSchema()(value);
+  }
+
+  set [3](value: S) {
+    this.e3 = this.getElementSchema()(value);
   }
 
   get x() {
@@ -494,10 +566,9 @@ abstract class Vec4<S> extends VecBase<S> implements Tuple4<S> {
 }
 
 export class Vec2fImpl extends Vec2<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return f32;
   }
-
   get kind() {
     return 'vec2f' as const;
   }
@@ -514,10 +585,9 @@ export class Vec2fImpl extends Vec2<number> {
 }
 
 export class Vec2hImpl extends Vec2<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return f16;
   }
-
   get kind() {
     return 'vec2h' as const;
   }
@@ -534,10 +604,9 @@ export class Vec2hImpl extends Vec2<number> {
 }
 
 export class Vec2iImpl extends Vec2<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return i32;
   }
-
   get kind() {
     return 'vec2i' as const;
   }
@@ -554,10 +623,9 @@ export class Vec2iImpl extends Vec2<number> {
 }
 
 export class Vec2uImpl extends Vec2<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return u32;
   }
-
   get kind() {
     return 'vec2u' as const;
   }
@@ -574,10 +642,9 @@ export class Vec2uImpl extends Vec2<number> {
 }
 
 export class Vec2bImpl extends Vec2<boolean> {
-  getDefaultValue() {
-    return false;
+  getElementSchema() {
+    return bool;
   }
-
   get kind() {
     return 'vec2<bool>' as const;
   }
@@ -594,10 +661,9 @@ export class Vec2bImpl extends Vec2<boolean> {
 }
 
 export class Vec3fImpl extends Vec3<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return f32;
   }
-
   get kind() {
     return 'vec3f' as const;
   }
@@ -614,10 +680,9 @@ export class Vec3fImpl extends Vec3<number> {
 }
 
 export class Vec3hImpl extends Vec3<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return f16;
   }
-
   get kind() {
     return 'vec3h' as const;
   }
@@ -634,10 +699,9 @@ export class Vec3hImpl extends Vec3<number> {
 }
 
 export class Vec3iImpl extends Vec3<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return i32;
   }
-
   get kind() {
     return 'vec3i' as const;
   }
@@ -654,10 +718,9 @@ export class Vec3iImpl extends Vec3<number> {
 }
 
 export class Vec3uImpl extends Vec3<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return u32;
   }
-
   get kind() {
     return 'vec3u' as const;
   }
@@ -674,10 +737,9 @@ export class Vec3uImpl extends Vec3<number> {
 }
 
 export class Vec3bImpl extends Vec3<boolean> {
-  getDefaultValue() {
-    return false;
+  getElementSchema() {
+    return bool;
   }
-
   get kind() {
     return 'vec3<bool>' as const;
   }
@@ -694,10 +756,9 @@ export class Vec3bImpl extends Vec3<boolean> {
 }
 
 export class Vec4fImpl extends Vec4<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return f32;
   }
-
   get kind() {
     return 'vec4f' as const;
   }
@@ -714,10 +775,9 @@ export class Vec4fImpl extends Vec4<number> {
 }
 
 export class Vec4hImpl extends Vec4<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return f16;
   }
-
   get kind() {
     return 'vec4h' as const;
   }
@@ -734,10 +794,9 @@ export class Vec4hImpl extends Vec4<number> {
 }
 
 export class Vec4iImpl extends Vec4<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return i32;
   }
-
   get kind() {
     return 'vec4i' as const;
   }
@@ -754,10 +813,9 @@ export class Vec4iImpl extends Vec4<number> {
 }
 
 export class Vec4uImpl extends Vec4<number> {
-  getDefaultValue() {
-    return 0;
+  getElementSchema() {
+    return u32;
   }
-
   get kind() {
     return 'vec4u' as const;
   }
@@ -774,10 +832,9 @@ export class Vec4uImpl extends Vec4<number> {
 }
 
 export class Vec4bImpl extends Vec4<boolean> {
-  getDefaultValue() {
-    return false;
+  getElementSchema() {
+    return bool;
   }
-
   get kind() {
     return 'vec4<bool>' as const;
   }
diff --git a/packages/typegpu/src/data/wgslTypes.ts b/packages/typegpu/src/data/wgslTypes.ts
index d66f41e3..c2e0777c 100644
--- a/packages/typegpu/src/data/wgslTypes.ts
+++ b/packages/typegpu/src/data/wgslTypes.ts
@@ -9,13 +9,13 @@ import type {
   MemIdentity,
   MemIdentityRecord,
 } from '../shared/repr.ts';
-import { $internal, $wgslDataType } from '../shared/symbols.ts';
 import type {
   $gpuRepr,
   $memIdent,
   $repr,
   $reprPartial,
 } from '../shared/symbols.ts';
+import { $internal, $wgslDataType } from '../shared/symbols.ts';
 import type { Prettify } from '../shared/utilityTypes.ts';
 
 type DecoratedLocation<T extends BaseData> = Decorated<T, Location[]>;
@@ -705,6 +705,8 @@ export interface Bool {
   // Type-tokens, not available at runtime
   readonly [$repr]: boolean;
   // ---
+
+  (v?: number | boolean): boolean;
 }
 
 /**
@@ -718,7 +720,7 @@ export interface F32 {
   readonly [$repr]: number;
   // ---
 
-  (v: number | boolean): number;
+  (v?: number | boolean): number;
 }
 
 /**
@@ -732,7 +734,7 @@ export interface F16 {
   readonly [$repr]: number;
   // ---
 
-  (v: number | boolean): number;
+  (v?: number | boolean): number;
 }
 
 /**
@@ -747,7 +749,7 @@ export interface I32 {
   readonly [$memIdent]: I32 | Atomic<I32> | DecoratedLocation<I32>;
   // ---
 
-  (v: number | boolean): number;
+  (v?: number | boolean): number;
 }
 
 /**
@@ -762,13 +764,12 @@ export interface U32 {
   readonly [$memIdent]: U32 | Atomic<U32> | DecoratedLocation<U32>;
   // ---
 
-  (v: number | boolean): number;
+  (v?: number | boolean): number;
 }
 
 /**
  * Unsigned 16-bit integer schema used exclusively for index buffer schemas.
  */
-
 export interface U16 {
   readonly [$internal]: true;
   readonly type: 'u16';


Test patch:
diff --git a/apps/typegpu-docs/src/pages/benchmark/test-suites/cast.ts b/apps/typegpu-docs/src/pages/benchmark/test-suites/cast.ts
new file mode 100644
index 00000000..f2b31f9f
--- /dev/null
+++ b/apps/typegpu-docs/src/pages/benchmark/test-suites/cast.ts
@@ -0,0 +1,36 @@
+import { Bench } from 'tinybench';
+import { stringifyLocator } from '../parameter-set.ts';
+import { createSuite } from '../suites.ts';
+
+export const castSuite = createSuite(
+  ({ params, d }) => {
+    const ctx = {
+      bench: null as unknown as Bench,
+      d,
+    };
+
+    ctx.bench = new Bench({
+      name: stringifyLocator('typegpu', params.typegpu),
+      time: 1000,
+    });
+
+    return ctx;
+  },
+  {
+    'f16 cast': (getCtx) => async () => {
+      const { d } = getCtx();
+
+      for (let i = 0; i < 10000; i++) {
+        d.f16(12333);
+      }
+    },
+
+    'f32 cast': (getCtx) => async () => {
+      const { d } = getCtx();
+
+      for (let i = 0; i < 10000; i++) {
+        d.f32(12333);
+      }
+    },
+  },
+);
diff --git a/packages/typegpu/tests/numeric.test.ts b/packages/typegpu/tests/numeric.test.ts
index 7d6f7329..cd662a91 100644
--- a/packages/typegpu/tests/numeric.test.ts
+++ b/packages/typegpu/tests/numeric.test.ts
@@ -1,5 +1,7 @@
-import { describe, it } from 'vitest';
+import { describe, expect, it } from 'vitest';
 import * as d from '../src/data/index.ts';
+import { tgpu } from '../src/index.ts';
+import { parse, parseResolved } from './utils/parseResolved.ts';
 
 describe('f32', () => {
   it('differs in type from other numeric schemas', () => {
@@ -48,3 +50,36 @@ describe('f16', () => {
     acceptsF16Schema(d.u32);
   });
 });
+
+it('has correct default values', () => {
+  expect(d.f32()).toBe(0);
+  expect(d.f16()).toBe(0);
+  expect(d.i32()).toBe(0);
+  expect(d.u32()).toBe(0);
+  expect(d.bool()).toBe(false);
+});
+
+describe('TGSL', () => {
+  it('works for default constructors', () => {
+    const main = tgpu
+      .fn([])(() => {
+        const f = d.f32();
+        const h = d.f16();
+        const i = d.i32();
+        const u = d.u32();
+        const b = d.bool();
+      });
+
+    expect(parseResolved({ main })).toBe(
+      parse(`
+      fn main() {
+        var f = f32();
+        var h = f16();
+        var i = i32();
+        var u = u32();
+        var b = bool();
+      }
+      `),
+    );
+  });
+});
diff --git a/packages/typegpu/tests/primitiveCast.test.ts b/packages/typegpu/tests/primitiveCast.test.ts
index 517f9c22..5f37c4c6 100644
--- a/packages/typegpu/tests/primitiveCast.test.ts
+++ b/packages/typegpu/tests/primitiveCast.test.ts
@@ -1,12 +1,13 @@
 import { describe, expect, it } from 'vitest';
-import { f16, f32, i32, u32 } from '../src/data/index.ts';
+import { bool, f16, f32, i32, u32 } from '../src/data/index.ts';
 
 describe('u32', () => {
   it('casts a number to u32', () => {
     expect(u32(10)).toBe(10);
     expect(u32(10.5)).toBe(10);
     expect(u32(-10)).toBe(4294967286);
-    expect(u32(-10.5)).toBe(0);
+    expect(u32(-10.5)).toBe(4294967286);
+    expect(u32(4294967295)).toBe(4294967295);
     expect(u32(4294967296)).toBe(0);
     expect(u32(4294967297)).toBe(1);
   });
@@ -23,8 +24,9 @@ describe('i32', () => {
     expect(i32(10.5)).toBe(10);
     expect(i32(-10)).toBe(-10);
     expect(i32(-10.5)).toBe(-10);
+    expect(i32(2147483647)).toBe(2147483647);
+    expect(i32(2147483648)).toBe(-2147483648);
     expect(i32(4294967286)).toBe(-10);
-    expect(i32(4294967286.5)).toBe(2147483647);
   });
 
   it('casts a boolean to i32', () => {
@@ -40,9 +42,6 @@ describe('f32', () => {
     expect(f32(-10)).toBe(-10);
     expect(f32(-10.5)).toBe(-10.5);
     expect(f32(4294967296)).toBe(4294967296);
-    expect(f32(4294967297)).toBe(4294967296); // 4294967297 is not exactly representable as an f32
-    expect(f32(4294967297.5)).toBe(4294967296); // 4294967297.5 is not exactly representable as an f32
-    expect(f32(4295029249)).toBe(4295029248); // 4295029249 is not exactly representable as an f32
   });
 
   it('casts a boolean to f32', () => {
@@ -52,19 +51,51 @@ describe('f32', () => {
 });
 
 describe('f16', () => {
-  it('casts a number to f16', () => {
+  it('casts typical finite numbers', () => {
     expect(f16(10)).toBe(10);
     expect(f16(10.5)).toBe(10.5);
     expect(f16(-10)).toBe(-10);
     expect(f16(-10.5)).toBe(-10.5);
-    expect(f16(65536)).toBe(Number.POSITIVE_INFINITY); // 65536 is too large to represent as an f16
-    expect(f16(65504)).toBe(65504); // 65504 is the largest representable number
-    expect(f16(65535)).toBe(65504); // the largest number that is rounded down instead of being infinity
-    expect(f16(-65536)).toBe(Number.NEGATIVE_INFINITY); // -65536 is too small to represent as an f16
-    expect(f16(-65505)).toBe(-65504); // -65504 is the smallest representable number
-
-    expect(f16(5475)).closeTo(5475, 4); // at this range, the precision is 4
-    expect(f16(5475)).not.toBe(5475); // the number is not exactly representable
+  });
+
+  it('handles the finite extrema and overflow', () => {
+    // Largest finite magnitude representable is ±65504
+    expect(f16(65504)).toBe(65504);
+    expect(f16(-65504)).toBe(-65504);
+
+    expect(f16(65536)).toBe(Number.POSITIVE_INFINITY);
+    expect(f16(65535)).toBe(Number.POSITIVE_INFINITY);
+    expect(f16(-65536)).toBe(Number.NEGATIVE_INFINITY);
+
+    // Values just above the max finite round down to the max finite (ties‑to‑even).
+    expect(f16(65505)).toBe(65504);
+    expect(f16(-65505)).toBe(-65504);
+  });
+
+  it('preserves special values and signed zeros', () => {
+    expect(f16(Number.POSITIVE_INFINITY)).toBe(Number.POSITIVE_INFINITY);
+    expect(f16(Number.NEGATIVE_INFINITY)).toBe(Number.NEGATIVE_INFINITY);
+    expect(Number.isNaN(f16(Number.NaN))).toBe(true);
+
+    expect(Object.is(f16(0), 0)).toBe(true);
+    expect(Object.is(f16(-0), -0)).toBe(true);
+  });
+
+  it('handles sub‑normals and underflow', () => {
+    // Smallest positive normal is 2^-14 ≈ 6.10352e‑5
+    expect(f16(6.10352e-5)).toBeCloseTo(6.10352e-5, 10);
+
+    // Smallest positive sub‑normal is 2^-24 ≈ 5.960464e‑8
+    const smallestSub = 2 ** -24;
+    expect(f16(smallestSub)).toBe(smallestSub);
+
+    // Anything smaller underflows to +0.
+    expect(f16(1e-8)).toBe(0);
+  });
+
+  it('rounds arbitrary fractions correctly', () => {
+    // 1/3 cannot be represented exactly; half precision rounds it to ≈ 0.333251953125.
+    expect(f16(1 / 3)).toBeCloseTo(0.333251953125, 12);
   });
 
   it('casts a boolean to f16', () => {
@@ -72,3 +103,11 @@ describe('f16', () => {
     expect(f16(false)).toBe(0);
   });
 });
+
+describe('bool', () => {
+  it('correctly coerces to booleans', () => {
+    expect(bool(1)).toBe(true);
+    expect(bool(0)).toBe(false);
+    expect(bool(-4.1)).toBe(true);
+  });
+});
diff --git a/packages/typegpu/tests/std/boolean/isCloseTo.test.ts b/packages/typegpu/tests/std/boolean/isCloseTo.test.ts
index 6e8b1d77..71fed390 100644
--- a/packages/typegpu/tests/std/boolean/isCloseTo.test.ts
+++ b/packages/typegpu/tests/std/boolean/isCloseTo.test.ts
@@ -32,6 +32,7 @@ describe('isCloseTo', () => {
       isCloseTo(
         vec4h(3.8, -4.87, -2.42, -1.97),
         vec4h(3.794, -4.861, -2.412, -1.971),
+        0.05,
       ),
     ).toBe(true);
   });
diff --git a/packages/typegpu/tests/std/numeric/mul.test.ts b/packages/typegpu/tests/std/numeric/mul.test.ts
index 2bc291e2..d345fc7e 100644
--- a/packages/typegpu/tests/std/numeric/mul.test.ts
+++ b/packages/typegpu/tests/std/numeric/mul.test.ts
@@ -97,8 +97,6 @@ describe('mul', () => {
   });
 
   it('computes product of a vec4 and vec4', () => {
-    expect(vec2f(1, 2)).toEqual(vec2u(1, 2));
-
     expect(mul(vec4f(1, 2, 3, 4), vec4f(3, 4, 5, 6))).toEqual(
       vec4f(3, 8, 15, 24),
     );
diff --git a/packages/typegpu/tests/vector.test.ts b/packages/typegpu/tests/vector.test.ts
index aa328da9..92a87a34 100644
--- a/packages/typegpu/tests/vector.test.ts
+++ b/packages/typegpu/tests/vector.test.ts
@@ -6,6 +6,41 @@ import { sizeOf } from '../src/data/sizeOf.ts';
 import tgpu from '../src/index.ts';
 import { parse, parseResolved } from './utils/parseResolved.ts';
 
+describe('constructors', () => {
+  it('casts floats to signed integers', () => {
+    expect(d.vec2i(1.1, -1.1)).toStrictEqual(d.vec2i(1, -1));
+    expect(d.vec3i(1.7, 2.6, 0.0)).toStrictEqual(d.vec3i(1, 2, 0));
+    expect(d.vec4i(1.1, -1.1, -10.2, -1.0)).toStrictEqual(
+      d.vec4i(1, -1, -10, -1),
+    );
+  });
+
+  it('casts floats to unsigned integers', () => {
+    expect(d.vec2u(1.1, -1)).toStrictEqual(d.vec2u(1, 4294967295));
+    expect(d.vec3u(1.7, 2.6, 0.0)).toStrictEqual(d.vec3u(1, 2, 0));
+    expect(d.vec4u(1.1, 1.1, 10.2, 1.0)).toStrictEqual(d.vec4u(1, 1, 10, 1));
+  });
+});
+
+describe('setters', () => {
+  it('coerces to singed integer values', () => {
+    const vec = d.vec4i();
+    vec[0] = 1.1;
+    vec[1] = -1.1;
+    vec.z = 2.2;
+    vec.w = -2.2;
+    expect(vec).toStrictEqual(d.vec4i(1, -1, 2, -2));
+  });
+
+  it('coerces to unsigned integer values', () => {
+    const vec = d.vec3u();
+    vec[0] = 1.1;
+    vec[1] = -1.1;
+    vec.z = 2.2;
+    expect(vec).toStrictEqual(d.vec3u(1, 4294967295, 2));
+  });
+});
+
 describe('vec2f', () => {
   it('should span 8 bytes', () => {
     expect(sizeOf(d.vec2f)).toBe(8);
@@ -832,13 +867,13 @@ describe('v3i', () => {
 describe('v4f', () => {
   describe('(v3f, number) constructor', () => {
     it('works in JS', () => {
-      const red = d.vec3f(0.9, 0.2, 0.1);
+      const red = d.vec3f(0.125, 0.25, 0.375);
       const redWithAlpha = d.vec4f(red, 1);
-      expect(redWithAlpha).toStrictEqual(d.vec4f(0.9, 0.2, 0.1, 1));
+      expect(redWithAlpha).toStrictEqual(d.vec4f(0.125, 0.25, 0.375, 1));
     });
 
     it('works in TGSL', () => {
-      const red = d.vec3f(0.9, 0.2, 0.1);
+      const red = d.vec3f(0.125, 0.25, 0.375);
 
       const main = tgpu.fn([])(() => {
         const green = d.vec3f(0, 1, 0);
@@ -853,7 +888,7 @@ describe('v4f', () => {
           fn main() {
             var green = vec3f(0, 1, 0);
 
-            var one = vec4f(vec3f(0.9, 0.2, 0.1), 1);
+            var one = vec4f(vec3f(0.125, 0.25, 0.375), 1);
             var two = vec4f(green, 1);
             var three = vec4f(vec3f(0, 0, 1), 1);
           }
@@ -864,30 +899,30 @@ describe('v4f', () => {
 
   describe('(number, v3f) constructor', () => {
     it('works in JS', () => {
-      const foo = d.vec3f(0.2, 0.3, 0.4);
+      const foo = d.vec3f(0.25, 0.5, 0.75);
       const bar = d.vec4f(0.1, foo);
-      expect(bar).toStrictEqual(d.vec4f(0.1, 0.2, 0.3, 0.4));
+      expect(bar).toStrictEqual(d.vec4f(0.1, 0.25, 0.5, 0.75));
     });
 
     it('works in TGSL', () => {
-      const foo = d.vec3f(0.2, 0.3, 0.4);
+      const foo = d.vec3f(0.25, 0.5, 0.75);
 
       const main = tgpu.fn([])(() => {
-        const fooLocal = d.vec3f(0.2, 0.3, 0.4);
+        const fooLocal = d.vec3f(0.25, 0.5, 0.75);
 
         const one = d.vec4f(0.1, foo); // external
         const two = d.vec4f(0.1, fooLocal); // local variable
-        const three = d.vec4f(0.1, d.vec3f(0.2, 0.3, 0.4)); // literal
+        const three = d.vec4f(0.1, d.vec3f(0.25, 0.5, 0.75)); // literal
       });
 
       expect(parseResolved({ main })).toBe(
         parse(`
         fn main() {
-          var fooLocal = vec3f(0.2, 0.3, 0.4);
+          var fooLocal = vec3f(0.25, 0.5, 0.75);
 
-          var one = vec4f(0.1, vec3f(0.2, 0.3, 0.4));
+          var one = vec4f(0.1, vec3f(0.25, 0.5, 0.75));
           var two = vec4f(0.1, fooLocal);
-          var three = vec4f(0.1, vec3f(0.2, 0.3, 0.4));
+          var three = vec4f(0.1, vec3f(0.25, 0.5, 0.75));
         }
       `),
       );