Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 25 additions & 29 deletions packages/typegpu/src/data/dataIO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TSchema extends wgsl.BaseData> = (
output: ISerialOutput,
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -488,7 +489,7 @@ const dataReaders = {
},

f16(input: ISerialInput): number {
return input.readFloat16();
return readFloat16(input);
},

i32(input: ISerialInput): number {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()),
Expand Down
11 changes: 11 additions & 0 deletions packages/typegpu/src/data/float16Conversion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +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(toHalfBits(value));
}

export function readFloat16(input: ISerialInput): number {
return fromHalfBits(input.readUint16());
}
39 changes: 27 additions & 12 deletions packages/typegpu/src/data/numeric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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;
}

/**
Expand Down
39 changes: 34 additions & 5 deletions packages/typegpu/src/std/bitcast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -205,12 +205,12 @@ const bufViews = {
f32: new Float32Array(buffer),
u32: new Uint32Array(buffer),
i32: new Int32Array(buffer),
f16: new Float16Array(buffer),
u16: new Uint16Array(buffer),
};

function writeToBuffer(
item: AnyNumericVecInstance | number,
target: Float32Array | Uint32Array | Int32Array | Float16Array,
target: Float32Array | Uint32Array | Int32Array,
): void {
if (typeof item === 'number') {
target[0] = item;
Expand All @@ -221,8 +221,18 @@ function writeToBuffer(
}
}

function writeFloat16ToBuffer(item: AnyNumericVecInstance | number, target: Uint16Array): void {
if (typeof item === 'number') {
target[0] = toHalfBits(item);
} else {
for (let i = 0; i < item.length; i++) {
target[i] = toHalfBits(item[i] as number);
}
}
}

function readFromBuffer<Schema extends BitcastAllowedTypes>(
buf: Float32Array | Uint32Array | Int32Array | Float16Array,
buf: Float32Array | Uint32Array | Int32Array,
schema: Schema,
): Infer<Schema> {
const length = 'componentCount' in schema ? schema.componentCount : 1;
Expand All @@ -233,6 +243,18 @@ function readFromBuffer<Schema extends BitcastAllowedTypes>(
return schema(...items) as Infer<Schema>;
}

function readFloat16FromBuffer<Schema extends BitcastAllowedTypes>(
buf: Uint16Array,
schema: Schema,
): Infer<Schema> {
const length = 'componentCount' in schema ? schema.componentCount : 1;
const items = [];
for (let i = 0; i < length; i++) {
items.push(fromHalfBits(buf[i] as number));
}
return schema(...items) as Infer<Schema>;
}

const getCpuBitcast = <In extends BitcastAllowedTypes, Out extends BitcastAllowedTypes>(
inType: In,
outType: Out,
Expand All @@ -242,7 +264,14 @@ const getCpuBitcast = <In extends BitcastAllowedTypes, Out extends BitcastAllowe
'primitive' in outType ? outType.primitive : outType;

return (value: Infer<In>): Infer<Out> => {
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);
};
};
Expand Down
7 changes: 4 additions & 3 deletions packages/typegpu/src/std/packing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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})`,
Expand All @@ -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());
},
Expand Down
Loading
Loading