From ceffbd4bd6f24c159712dad1013c0902bdb9298f Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Thu, 30 Jul 2026 22:19:53 +0200 Subject: [PATCH 1/3] fix: Manual float16 conversions to extend platform support --- packages/typegpu/src/data/dataIO.ts | 54 +++++++++---------- .../typegpu/src/data/float16Conversion.ts | 42 +++++++++++++++ packages/typegpu/src/std/bitcast.ts | 34 +++++++++++- packages/typegpu/src/std/packing.ts | 7 +-- 4 files changed, 103 insertions(+), 34 deletions(-) create mode 100644 packages/typegpu/src/data/float16Conversion.ts diff --git a/packages/typegpu/src/data/dataIO.ts b/packages/typegpu/src/data/dataIO.ts index c0b8bfdc25..fa4904324e 100644 --- a/packages/typegpu/src/data/dataIO.ts +++ b/packages/typegpu/src/data/dataIO.ts @@ -32,6 +32,7 @@ import { getCompiledWriter } from './compiledIO.ts'; import { getName } from '../shared/meta.ts'; import { roundUp } from '../mathUtils.ts'; import { logger } from '../tgpuLogger.ts'; +import { readFloat16, writeFloat16 } from './float16Conversion.ts'; type DataWriter = ( output: ISerialOutput, @@ -62,7 +63,7 @@ const dataWriters = { }, f16(output, _schema: wgsl.F16, value: number) { - output.writeFloat16(value); + writeFloat16(output, value); }, i32(output, _schema: wgsl.I32, value: number) { @@ -83,8 +84,8 @@ const dataWriters = { }, vec2h(output, _, value: wgsl.v2h) { - output.writeFloat16(value[0]); - output.writeFloat16(value[1]); + writeFloat16(output, value[0]); + writeFloat16(output, value[1]); }, vec2i(output, _, value: wgsl.v2i) { @@ -108,9 +109,9 @@ const dataWriters = { }, vec3h(output, _, value: wgsl.v3h) { - output.writeFloat16(value[0]); - output.writeFloat16(value[1]); - output.writeFloat16(value[2]); + writeFloat16(output, value[0]); + writeFloat16(output, value[1]); + writeFloat16(output, value[2]); }, vec3i(output, _, value: wgsl.v3i) { @@ -137,10 +138,10 @@ const dataWriters = { }, vec4h(output, _, value: wgsl.v4h) { - output.writeFloat16(value[0]); - output.writeFloat16(value[1]); - output.writeFloat16(value[2]); - output.writeFloat16(value[3]); + writeFloat16(output, value[0]); + writeFloat16(output, value[1]); + writeFloat16(output, value[2]); + writeFloat16(output, value[3]); }, vec4i(output, _, value: wgsl.v4i) { @@ -330,17 +331,17 @@ const dataWriters = { output.writeInt16(Math.round(value.w * 32767)); }, float16(output, _, value: number) { - output.writeFloat16(value); + writeFloat16(output, value); }, float16x2(output, _, value: wgsl.v2f) { - output.writeFloat16(value.x); - output.writeFloat16(value.y); + writeFloat16(output, value.x); + writeFloat16(output, value.y); }, float16x4(output, _, value: wgsl.v4f) { - output.writeFloat16(value.x); - output.writeFloat16(value.y); - output.writeFloat16(value.z); - output.writeFloat16(value.w); + writeFloat16(output, value.x); + writeFloat16(output, value.y); + writeFloat16(output, value.z); + writeFloat16(output, value.w); }, float32(output, _, value: number) { output.writeFloat32(value); @@ -488,7 +489,7 @@ const dataReaders = { }, f16(input: ISerialInput): number { - return input.readFloat16(); + return readFloat16(input); }, i32(input: ISerialInput): number { @@ -521,20 +522,15 @@ const dataReaders = { }, vec2h(input): wgsl.v2h { - return vec2h(input.readFloat16(), input.readFloat16()); + return vec2h(readFloat16(input), readFloat16(input)); }, vec3h(input: ISerialInput): wgsl.v3h { - return vec3h(input.readFloat16(), input.readFloat16(), input.readFloat16()); + return vec3h(readFloat16(input), readFloat16(input), readFloat16(input)); }, vec4h(input: ISerialInput): wgsl.v4h { - return vec4h( - input.readFloat16(), - input.readFloat16(), - input.readFloat16(), - input.readFloat16(), - ); + return vec4h(readFloat16(input), readFloat16(input), readFloat16(input), readFloat16(input)); }, vec2i(input): wgsl.v2i { @@ -723,10 +719,10 @@ const dataReaders = { i.readInt16() / 32767, ), float16(i) { - return i.readFloat16(); + return readFloat16(i); }, - float16x2: (i) => vec2f(i.readFloat16(), i.readFloat16()), - float16x4: (i) => vec4f(i.readFloat16(), i.readFloat16(), i.readFloat16(), i.readFloat16()), + float16x2: (i) => vec2f(readFloat16(i), readFloat16(i)), + float16x4: (i) => vec4f(readFloat16(i), readFloat16(i), readFloat16(i), readFloat16(i)), float32: (i) => i.readFloat32(), float32x2: (i) => vec2f(i.readFloat32(), i.readFloat32()), float32x3: (i) => vec3f(i.readFloat32(), i.readFloat32(), i.readFloat32()), diff --git a/packages/typegpu/src/data/float16Conversion.ts b/packages/typegpu/src/data/float16Conversion.ts new file mode 100644 index 0000000000..b1339fe8f8 --- /dev/null +++ b/packages/typegpu/src/data/float16Conversion.ts @@ -0,0 +1,42 @@ +import type { ISerialInput } from 'typed-binary'; +import type { ISerialOutput } from 'typed-binary'; + +export function writeFloat16(output: ISerialOutput, value: number): void { + output.writeUint16(encodeFloat16AsUint16(value)); +} + +export function readFloat16(input: ISerialInput): number { + return decodeUint16AsFloat16(input.readUint16()); +} + +export function encodeFloat16AsUint16(value: number): number { + // conversion according to IEEE 754 binary16 format + if (value === 0) return 0; + if (Number.isNaN(value)) return 0x7e00; + if (!Number.isFinite(value)) return value > 0 ? 0x7c00 : 0xfc00; + + const sign = value < 0 ? 1 : 0; + const absValue = Math.abs(value); + const exponent = Math.floor(Math.log2(absValue)); + const mantissa = absValue / 2 ** exponent - 1; + const biasedExponent = exponent + 15; + const mantissaBits = Math.floor(mantissa * 1024); + return (sign << 15) | (biasedExponent << 10) | mantissaBits; +} + +export function decodeUint16AsFloat16(uint16Encoding: number): number { + const sign = (uint16Encoding & 0x8000) >> 15; + const exponent = (uint16Encoding & 0x7c00) >> 10; + const mantissa = uint16Encoding & 0x3ff; + if (exponent === 0) { + return sign === 0 ? mantissa / 1024 : -mantissa / 1024; + } + if (exponent === 31) { + return mantissa === 0 + ? sign === 0 + ? Number.POSITIVE_INFINITY + : Number.NEGATIVE_INFINITY + : Number.NaN; + } + return (sign === 0 ? 1 : -1) * (1 + mantissa / 1024) * 2 ** (exponent - 15); +} diff --git a/packages/typegpu/src/std/bitcast.ts b/packages/typegpu/src/std/bitcast.ts index 755ee9ee54..92bdca9693 100644 --- a/packages/typegpu/src/std/bitcast.ts +++ b/packages/typegpu/src/std/bitcast.ts @@ -44,6 +44,7 @@ import { SignatureNotSupportedError } from '../errors.ts'; import { getName } from '../internal.ts'; import type { Infer } from '../shared/repr.ts'; import { comptime } from '../core/function/comptime.ts'; +import { decodeUint16AsFloat16, encodeFloat16AsUint16 } from '../data/float16Conversion.ts'; type BitcastU32toF32Overload = ( value: T, @@ -205,7 +206,7 @@ const bufViews = { f32: new Float32Array(buffer), u32: new Uint32Array(buffer), i32: new Int32Array(buffer), - f16: new Float16Array(buffer), + u16: new Uint16Array(buffer), }; function writeToBuffer( @@ -221,6 +222,16 @@ function writeToBuffer( } } +function writeFloat16ToBuffer(item: AnyNumericVecInstance | number, target: Uint16Array): void { + if (typeof item === 'number') { + target[0] = encodeFloat16AsUint16(item); + } else { + for (let i = 0; i < item.length; i++) { + target[i] = encodeFloat16AsUint16(item[i] as number); + } + } +} + function readFromBuffer( buf: Float32Array | Uint32Array | Int32Array | Float16Array, schema: Schema, @@ -233,6 +244,18 @@ function readFromBuffer( return schema(...items) as Infer; } +function readFloat16FromBuffer( + buf: Uint16Array, + schema: Schema, +): Infer { + const length = 'componentCount' in schema ? schema.componentCount : 1; + const items = []; + for (let i = 0; i < length; i++) { + items.push(decodeUint16AsFloat16(buf[i] as number)); + } + return schema(...items) as Infer; +} + const getCpuBitcast = ( inType: In, outType: Out, @@ -242,7 +265,14 @@ const getCpuBitcast = ): Infer => { - writeToBuffer(value, bufViews[writeToPrimitive.type]); + if (writeToPrimitive.type === 'f16') { + writeFloat16ToBuffer(value, bufViews['u16']); + } else { + writeToBuffer(value, bufViews[writeToPrimitive.type]); + } + if (readFromPrimitive.type === 'f16') { + return readFloat16FromBuffer(bufViews['u16'], outType); + } return readFromBuffer(bufViews[readFromPrimitive.type], outType); }; }; diff --git a/packages/typegpu/src/std/packing.ts b/packages/typegpu/src/std/packing.ts index f4f990e951..3d99fb2a26 100644 --- a/packages/typegpu/src/std/packing.ts +++ b/packages/typegpu/src/std/packing.ts @@ -4,6 +4,7 @@ import { stitch } from '../core/resolve/stitch.ts'; import { u32 } from '../data/numeric.ts'; import { vec2f, vec4f } from '../data/vector.ts'; import type { v2f, v4f } from '../data/wgslTypes.ts'; +import { readFloat16, writeFloat16 } from '../data/float16Conversion.ts'; /** * @privateRemarks @@ -16,7 +17,7 @@ export const unpack2x16float = dualImpl({ const writer = new TB.BufferWriter(buffer); writer.writeUint32(e); const reader = new TB.BufferReader(buffer); - return vec2f(reader.readFloat16(), reader.readFloat16()); + return vec2f(readFloat16(reader), readFloat16(reader)); }, signature: { argTypes: [u32], returnType: vec2f }, codegenImpl: (_ctx, [e]) => stitch`unpack2x16float(${e})`, @@ -32,8 +33,8 @@ export const pack2x16float = dualImpl({ normalImpl: (e: v2f): number => { const buffer = new ArrayBuffer(4); const writer = new TB.BufferWriter(buffer); - writer.writeFloat16(e.x); - writer.writeFloat16(e.y); + writeFloat16(writer, e.x); + writeFloat16(writer, e.y); const reader = new TB.BufferReader(buffer); return u32(reader.readUint32()); }, From 7fd50d233ba17267bb9767b8f798c12d943b519f Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Fri, 31 Jul 2026 12:32:32 +0200 Subject: [PATCH 2/3] Simpler --- .../typegpu/src/data/float16Conversion.ts | 37 +---- packages/typegpu/src/data/numeric.ts | 39 +++-- packages/typegpu/src/std/bitcast.ts | 13 +- .../typegpu/tests/internal/halfBits.test.ts | 137 ++++++++++++++++++ 4 files changed, 173 insertions(+), 53 deletions(-) create mode 100644 packages/typegpu/tests/internal/halfBits.test.ts diff --git a/packages/typegpu/src/data/float16Conversion.ts b/packages/typegpu/src/data/float16Conversion.ts index b1339fe8f8..0f40e42a4d 100644 --- a/packages/typegpu/src/data/float16Conversion.ts +++ b/packages/typegpu/src/data/float16Conversion.ts @@ -1,42 +1,11 @@ import type { ISerialInput } from 'typed-binary'; import type { ISerialOutput } from 'typed-binary'; +import { fromHalfBits, toHalfBits } from './numeric.ts'; export function writeFloat16(output: ISerialOutput, value: number): void { - output.writeUint16(encodeFloat16AsUint16(value)); + output.writeUint16(toHalfBits(value)); } export function readFloat16(input: ISerialInput): number { - return decodeUint16AsFloat16(input.readUint16()); -} - -export function encodeFloat16AsUint16(value: number): number { - // conversion according to IEEE 754 binary16 format - if (value === 0) return 0; - if (Number.isNaN(value)) return 0x7e00; - if (!Number.isFinite(value)) return value > 0 ? 0x7c00 : 0xfc00; - - const sign = value < 0 ? 1 : 0; - const absValue = Math.abs(value); - const exponent = Math.floor(Math.log2(absValue)); - const mantissa = absValue / 2 ** exponent - 1; - const biasedExponent = exponent + 15; - const mantissaBits = Math.floor(mantissa * 1024); - return (sign << 15) | (biasedExponent << 10) | mantissaBits; -} - -export function decodeUint16AsFloat16(uint16Encoding: number): number { - const sign = (uint16Encoding & 0x8000) >> 15; - const exponent = (uint16Encoding & 0x7c00) >> 10; - const mantissa = uint16Encoding & 0x3ff; - if (exponent === 0) { - return sign === 0 ? mantissa / 1024 : -mantissa / 1024; - } - if (exponent === 31) { - return mantissa === 0 - ? sign === 0 - ? Number.POSITIVE_INFINITY - : Number.NEGATIVE_INFINITY - : Number.NaN; - } - return (sign === 0 ? 1 : -1) * (1 + mantissa / 1024) * 2 ** (exponent - 15); + return fromHalfBits(input.readUint16()); } diff --git a/packages/typegpu/src/data/numeric.ts b/packages/typegpu/src/data/numeric.ts index 075f8d5014..f40e710ff7 100644 --- a/packages/typegpu/src/data/numeric.ts +++ b/packages/typegpu/src/data/numeric.ts @@ -185,7 +185,7 @@ export function toHalfBits(x: number): number { // 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. + const mant = bits & 0x7fffff; // Bits 22‑0 are the significand. // 2. Handle special values (NaN, ±∞) before re‑biasing. if (exp === 0xff) { @@ -198,15 +198,25 @@ export function toHalfBits(x: number): number { // 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. + // Below the smallest representable subnormal magnitude, round 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; + // Produce a sub‑normal: prepend the hidden 1, then round to nearest, + // ties to even. `shift` is the number of low bits dropped from the + // 24‑bit significand; the bit just below it is the rounding bit and + // everything under that forms the sticky bit. + const full = mant | 0x800000; // 24-bit significand incl. the implicit 1. + const shift = 14 - exp; // in [14, 24] + const roundBit = (full >> (shift - 1)) & 1; + const sticky = full & ((1 << (shift - 1)) - 1) ? 1 : 0; + let half = full >>> shift; + if (roundBit && (sticky || half & 1)) { + half += 1; // A carry here promotes to the smallest normal — that's fine, + // the bit pattern (exp field 1, mant 0) is exactly 2^-14. + } + return (sign << 15) | half; } // 5. Overflow: if the biased exponent is 31 (0x1f) or higher, the number @@ -215,18 +225,23 @@ export function toHalfBits(x: number): number { 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). + // 6. Normalised number: round mantissa to nearest, ties to even, then pack. + const roundBit = (mant >> 12) & 1; + const sticky = mant & 0xfff ? 1 : 0; + let half = mant >>> 13; + if (roundBit && (sticky || half & 1)) { + half += 1; + } + if (half === 0x400) { + // The carry propagated out of the 10‑bit mantissa; it overflowed. + half = 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); + return (sign << 15) | (exp << 10) | half; } /** diff --git a/packages/typegpu/src/std/bitcast.ts b/packages/typegpu/src/std/bitcast.ts index 92bdca9693..208a168274 100644 --- a/packages/typegpu/src/std/bitcast.ts +++ b/packages/typegpu/src/std/bitcast.ts @@ -5,7 +5,7 @@ import { bitcastU32toF32Impl, bitcastU32toI32Impl, } from '../data/numberOps.ts'; -import { f16, f32, i32, u32 } from '../data/numeric.ts'; +import { f16, f32, fromHalfBits, i32, toHalfBits, u32 } from '../data/numeric.ts'; import { isVec } from '../data/wgslTypes.ts'; import { vec2f, @@ -44,7 +44,6 @@ import { SignatureNotSupportedError } from '../errors.ts'; import { getName } from '../internal.ts'; import type { Infer } from '../shared/repr.ts'; import { comptime } from '../core/function/comptime.ts'; -import { decodeUint16AsFloat16, encodeFloat16AsUint16 } from '../data/float16Conversion.ts'; type BitcastU32toF32Overload = ( value: T, @@ -211,7 +210,7 @@ const bufViews = { function writeToBuffer( item: AnyNumericVecInstance | number, - target: Float32Array | Uint32Array | Int32Array | Float16Array, + target: Float32Array | Uint32Array | Int32Array, ): void { if (typeof item === 'number') { target[0] = item; @@ -224,16 +223,16 @@ function writeToBuffer( function writeFloat16ToBuffer(item: AnyNumericVecInstance | number, target: Uint16Array): void { if (typeof item === 'number') { - target[0] = encodeFloat16AsUint16(item); + target[0] = toHalfBits(item); } else { for (let i = 0; i < item.length; i++) { - target[i] = encodeFloat16AsUint16(item[i] as number); + target[i] = toHalfBits(item[i] as number); } } } function readFromBuffer( - buf: Float32Array | Uint32Array | Int32Array | Float16Array, + buf: Float32Array | Uint32Array | Int32Array, schema: Schema, ): Infer { const length = 'componentCount' in schema ? schema.componentCount : 1; @@ -251,7 +250,7 @@ function readFloat16FromBuffer( const length = 'componentCount' in schema ? schema.componentCount : 1; const items = []; for (let i = 0; i < length; i++) { - items.push(decodeUint16AsFloat16(buf[i] as number)); + items.push(fromHalfBits(buf[i] as number)); } return schema(...items) as Infer; } diff --git a/packages/typegpu/tests/internal/halfBits.test.ts b/packages/typegpu/tests/internal/halfBits.test.ts new file mode 100644 index 0000000000..29f140395f --- /dev/null +++ b/packages/typegpu/tests/internal/halfBits.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it } from 'vitest'; +import { fromHalfBits, toHalfBits } from '../../src/data/numeric.ts'; + +// Reference conversion using the native Float16Array (available in the test +// runtime), used to cross-check the manual implementation. +const nativeToHalfBits = (value: number): number => { + const f16 = new Float16Array(1); + const u16 = new Uint16Array(f16.buffer); + f16[0] = value; + return u16[0] as number; +}; + +describe('toHalfBits', () => { + it('encodes zero and negative zero', () => { + expect(toHalfBits(0)).toBe(0x0000); + expect(toHalfBits(-0)).toBe(0x8000); + }); + + it('encodes NaN and infinities', () => { + expect(toHalfBits(Number.NaN) & 0x7c00).toBe(0x7c00); + expect(toHalfBits(Number.NaN) & 0x03ff).not.toBe(0); + expect(toHalfBits(Number.POSITIVE_INFINITY)).toBe(0x7c00); + expect(toHalfBits(Number.NEGATIVE_INFINITY)).toBe(0xfc00); + }); + + it('encodes normal values', () => { + expect(toHalfBits(1)).toBe(0x3c00); + expect(toHalfBits(-1)).toBe(0xbc00); + expect(toHalfBits(2)).toBe(0x4000); + expect(toHalfBits(0.5)).toBe(0x3800); + }); + + it('encodes the maximum finite half value', () => { + expect(toHalfBits(65504)).toBe(0x7bff); + }); + + it('clamps overflow to infinity', () => { + // Above the max finite half value (65504) but finite as f32. + expect(toHalfBits(70000)).toBe(0x7c00); + expect(toHalfBits(-70000)).toBe(0xfc00); + expect(toHalfBits(1e30)).toBe(0x7c00); + }); + + it('encodes subnormals', () => { + // Smallest positive subnormal: 2^-24. + expect(toHalfBits(2 ** -24)).toBe(0x0001); + // Largest subnormal: (1023/1024) * 2^-14. + expect(toHalfBits((1023 / 1024) * 2 ** -14)).toBe(0x03ff); + // Smallest positive normal: 2^-14. + expect(toHalfBits(2 ** -14)).toBe(0x0400); + }); + + it('underflows tiny magnitudes to zero', () => { + // Below half of the smallest subnormal (2^-25) rounds to zero. + expect(toHalfBits(2 ** -30)).toBe(0x0000); + expect(toHalfBits(-(2 ** -30))).toBe(0x8000); + }); + + it('rounds to nearest even', () => { + // 1 + 3/2048 sits exactly between 0x3c01 and 0x3c02; ties round to even. + const value = 1 + 3 / 2048; + expect(toHalfBits(value)).toBe(nativeToHalfBits(value)); + }); + + it('matches the native Float16Array conversion across a range', () => { + const samples = [ + 0, + -0, + 1, + -1, + 0.1, + -0.1, + 1.23456, + 65504, + 65505, + 100000, + 1e-5, + 6.1e-5, + 2 ** -14, + 2 ** -24, + 2 ** -25, + 2 ** -30, + 12345.6, + -12345.6, + ]; + for (const s of samples) { + expect(toHalfBits(s)).toBe(nativeToHalfBits(s)); + } + }); +}); + +describe('fromHalfBits', () => { + it('decodes zero and negative zero', () => { + expect(fromHalfBits(0x0000)).toBe(0); + expect(Object.is(fromHalfBits(0x8000), -0)).toBe(true); + }); + + it('decodes NaN and infinities', () => { + expect(fromHalfBits(0x7e00)).toBeNaN(); + expect(fromHalfBits(0x7c00)).toBe(Number.POSITIVE_INFINITY); + expect(fromHalfBits(0xfc00)).toBe(Number.NEGATIVE_INFINITY); + }); + + it('decodes normal values', () => { + expect(fromHalfBits(0x3c00)).toBe(1); + expect(fromHalfBits(0xbc00)).toBe(-1); + expect(fromHalfBits(0x7bff)).toBe(65504); + }); + + it('decodes subnormals with the 2^-14 scaling', () => { + // 0x0001 is the smallest subnormal: 2^-24, NOT 1/1024. + expect(fromHalfBits(0x0001)).toBe(2 ** -24); + expect(fromHalfBits(0x03ff)).toBeCloseTo((1023 / 1024) * 2 ** -14, 20); + }); +}); + +describe('round-trip', () => { + it('is stable for representable values', () => { + const values = [ + 0, + -0, + 1, + -1, + 0.5, + 2, + 65504, + 2 ** -14, + 2 ** -24, + Number.POSITIVE_INFINITY, + Number.NEGATIVE_INFINITY, + ]; + for (const v of values) { + const roundTripped = fromHalfBits(toHalfBits(v)); + expect(Object.is(roundTripped, v)).toBe(true); + } + }); +}); From 921f0078e03533ef0b899de7e9be53cd30ddae16 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Wed, 5 Aug 2026 14:46:47 +0200 Subject: [PATCH 3/3] Review fixes --- packages/typegpu/src/data/numeric.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/typegpu/src/data/numeric.ts b/packages/typegpu/src/data/numeric.ts index f40e710ff7..2fed5e2135 100644 --- a/packages/typegpu/src/data/numeric.ts +++ b/packages/typegpu/src/data/numeric.ts @@ -209,10 +209,10 @@ export function toHalfBits(x: number): number { // everything under that forms the sticky bit. const full = mant | 0x800000; // 24-bit significand incl. the implicit 1. const shift = 14 - exp; // in [14, 24] - const roundBit = (full >> (shift - 1)) & 1; + const roundBit = (full >>> (shift - 1)) & 1; const sticky = full & ((1 << (shift - 1)) - 1) ? 1 : 0; let half = full >>> shift; - if (roundBit && (sticky || half & 1)) { + if (roundBit & (sticky | (half & 1))) { half += 1; // A carry here promotes to the smallest normal — that's fine, // the bit pattern (exp field 1, mant 0) is exactly 2^-14. } @@ -226,10 +226,10 @@ export function toHalfBits(x: number): number { } // 6. Normalised number: round mantissa to nearest, ties to even, then pack. - const roundBit = (mant >> 12) & 1; + const roundBit = (mant >>> 12) & 1; const sticky = mant & 0xfff ? 1 : 0; let half = mant >>> 13; - if (roundBit && (sticky || half & 1)) { + if (roundBit & (sticky | (half & 1))) { half += 1; } if (half === 0x400) {