diff --git a/src/common/framework/data_cache.ts b/src/common/framework/data_cache.ts index 6f6e80288a8f..be5bde8e224b 100644 --- a/src/common/framework/data_cache.ts +++ b/src/common/framework/data_cache.ts @@ -3,15 +3,64 @@ * expensive to build using a two-level cache (in-memory, pre-computed file). */ +import { assert } from '../util/util.js'; + interface DataStore { - load(path: string): Promise; + load(path: string): Promise; } /** Logger is a basic debug logger function */ export type Logger = (s: string) => void; -/** DataCache is an interface to a data store used to hold cached data */ +/** + * DataCacheNode represents a single cache entry in the LRU DataCache. + * DataCacheNode is a doubly linked list, so that least-recently-used entries can be removed, and + * cache hits can move the node to the front of the list. + */ +class DataCacheNode { + public constructor(path: string, data: unknown) { + this.path = path; + this.data = data; + } + + /** insertAfter() re-inserts this node in the doubly-linked list after @p prev */ + public insertAfter(prev: DataCacheNode) { + this.unlink(); + this.next = prev.next; + this.prev = prev; + prev.next = this; + if (this.next) { + this.next.prev = this; + } + } + + /** unlink() removes this node from the doubly-linked list */ + public unlink() { + const prev = this.prev; + const next = this.next; + if (prev) { + prev.next = next; + } + if (next) { + next.prev = prev; + } + this.prev = null; + this.next = null; + } + + public readonly path: string; // The file path this node represents + public readonly data: unknown; // The deserialized data for this node + public prev: DataCacheNode | null = null; // The previous node in the doubly-linked list + public next: DataCacheNode | null = null; // The next node in the doubly-linked list +} + +/** DataCache is an interface to a LRU-cached data store used to hold data cached by path */ export class DataCache { + public constructor() { + this.lruHeadNode.next = this.lruTailNode; + this.lruTailNode.prev = this.lruHeadNode; + } + /** setDataStore() sets the backing data store used by the data cache */ public setStore(dataStore: DataStore) { this.dataStore = dataStore; @@ -28,17 +77,20 @@ export class DataCache { * building the data and storing it in the cache. */ public async fetch(cacheable: Cacheable): Promise { - // First check the in-memory cache - let data = this.cache.get(cacheable.path); - if (data !== undefined) { - this.log('in-memory cache hit'); - return Promise.resolve(data as Data); + { + // First check the in-memory cache + const node = this.cache.get(cacheable.path); + if (node !== undefined) { + this.log('in-memory cache hit'); + node.insertAfter(this.lruHeadNode); + return Promise.resolve(node.data as Data); + } } this.log('in-memory cache miss'); // In in-memory cache miss. // Next, try the data store. if (this.dataStore !== null && !this.unavailableFiles.has(cacheable.path)) { - let serialized: string | undefined; + let serialized: Uint8Array | undefined; try { serialized = await this.dataStore.load(cacheable.path); this.log('loaded serialized'); @@ -49,16 +101,37 @@ export class DataCache { } if (serialized !== undefined) { this.log(`deserializing`); - data = cacheable.deserialize(serialized); - this.cache.set(cacheable.path, data); - return data as Data; + const data = cacheable.deserialize(serialized); + this.addToCache(cacheable.path, data); + return data; } } // Not found anywhere. Build the data, and cache for future lookup. this.log(`cache: building (${cacheable.path})`); - data = await cacheable.build(); - this.cache.set(cacheable.path, data); - return data as Data; + const data = await cacheable.build(); + this.addToCache(cacheable.path, data); + return data; + } + + /** + * addToCache() creates a new node for @p path and @p data, inserting the new node at the front of + * the doubly-linked list. If the number of entries in the cache exceeds this.maxCount, then the + * least recently used entry is evicted + * @param path the file path for the data + * @param data the deserialized data + */ + private addToCache(path: string, data: unknown) { + if (this.cache.size >= this.maxCount) { + const toEvict = this.lruTailNode.prev; + assert(toEvict !== null); + toEvict.unlink(); + this.cache.delete(toEvict.path); + this.log(`evicting ${toEvict.path}`); + } + const node = new DataCacheNode(path, data); + node.insertAfter(this.lruHeadNode); + this.cache.set(path, node); + this.log(`added ${path}. new count: ${this.cache.size}`); } private log(msg: string) { @@ -67,7 +140,12 @@ export class DataCache { } } - private cache = new Map(); + // Max number of entries in the cache before LRU entries are evicted. + private readonly maxCount = 4; + + private cache = new Map(); + private lruHeadNode = new DataCacheNode('', null); // placeholder node (no path or data) + private lruTailNode = new DataCacheNode('', null); // placeholder node (no path or data) private unavailableFiles = new Set(); private dataStore: DataStore | null = null; private debugLogger: Logger | null = null; @@ -107,14 +185,13 @@ export interface Cacheable { build(): Promise; /** - * serialize() transforms `data` to a string (usually JSON encoded) so that it - * can be stored in a text cache file. + * serialize() encodes `data` to a binary representation so that it can be stored in a cache file. */ - serialize(data: Data): string; + serialize(data: Data): Uint8Array; /** - * deserialize() is the inverse of serialize(), transforming the string back - * to the Data object. + * deserialize() is the inverse of serialize(), decoding the binary representation back to a Data + * object. */ - deserialize(serialized: string): Data; + deserialize(binary: Uint8Array): Data; } diff --git a/src/common/runtime/cmdline.ts b/src/common/runtime/cmdline.ts index 1fb39b68ce8c..44a73fb38b34 100644 --- a/src/common/runtime/cmdline.ts +++ b/src/common/runtime/cmdline.ts @@ -135,8 +135,8 @@ Did you remember to build with code coverage instrumentation enabled?` if (dataPath !== undefined) { dataCache.setStore({ load: (path: string) => { - return new Promise((resolve, reject) => { - fs.readFile(`${dataPath}/${path}`, 'utf8', (err, data) => { + return new Promise((resolve, reject) => { + fs.readFile(`${dataPath}/${path}`, (err, data) => { if (err !== null) { reject(err.message); } else { diff --git a/src/common/runtime/server.ts b/src/common/runtime/server.ts index 8903d5a53293..8310784e3a2c 100644 --- a/src/common/runtime/server.ts +++ b/src/common/runtime/server.ts @@ -133,8 +133,8 @@ Did you remember to build with code coverage instrumentation enabled?` if (dataPath !== undefined) { dataCache.setStore({ load: (path: string) => { - return new Promise((resolve, reject) => { - fs.readFile(`${dataPath}/${path}`, 'utf8', (err, data) => { + return new Promise((resolve, reject) => { + fs.readFile(`${dataPath}/${path}`, (err, data) => { if (err !== null) { reject(err.message); } else { diff --git a/src/common/runtime/standalone.ts b/src/common/runtime/standalone.ts index 4ec300d30684..be5887c1721e 100644 --- a/src/common/runtime/standalone.ts +++ b/src/common/runtime/standalone.ts @@ -84,7 +84,7 @@ dataCache.setStore({ if (!response.ok) { return Promise.reject(response.statusText); } - return await response.text(); + return new Uint8Array(await response.arrayBuffer()); }, }); diff --git a/src/common/tools/gen_cache.ts b/src/common/tools/gen_cache.ts index 4d1a9da726da..ce0854aa2046 100644 --- a/src/common/tools/gen_cache.ts +++ b/src/common/tools/gen_cache.ts @@ -87,8 +87,8 @@ const outRootDir = nonFlagsArgs[2]; dataCache.setStore({ load: (path: string) => { - return new Promise((resolve, reject) => { - fs.readFile(`data/${path}`, 'utf8', (err, data) => { + return new Promise((resolve, reject) => { + fs.readFile(`data/${path}`, (err, data) => { if (err !== null) { reject(err.message); } else { @@ -180,7 +180,7 @@ and const data = await cacheable.build(); const serialized = cacheable.serialize(data); fs.mkdirSync(path.dirname(outPath), { recursive: true }); - fs.writeFileSync(outPath, serialized); + fs.writeFileSync(outPath, serialized, 'binary'); break; } case 'list': { diff --git a/src/unittests/serialization.spec.ts b/src/unittests/serialization.spec.ts index 25aa44561fab..76ac1f715545 100644 --- a/src/unittests/serialization.spec.ts +++ b/src/unittests/serialization.spec.ts @@ -7,6 +7,7 @@ import { deserializeExpectation, serializeExpectation, } from '../webgpu/shader/execution/expression/case_cache.js'; +import BinaryStream from '../webgpu/util/binary_stream.js'; import { anyOf, deserializeComparator, @@ -206,11 +207,14 @@ g.test('value').fn(t => { f32 ), ]) { - const serialized = serializeValue(value); - const deserialized = deserializeValue(serialized); + const s = new BinaryStream(new Uint8Array(1024)); + serializeValue(s, value); + const d = new BinaryStream(s.buffer()); + const deserialized = deserializeValue(d); t.expect( objectEquals(value, deserialized), - `value ${value} -> serialize -> deserialize -> ${deserialized}` + `${value.type} ${value} -> serialize -> deserialize -> ${deserialized} +buffer: ${s.buffer()}` ); } }); @@ -240,8 +244,10 @@ g.test('fpinterval_f32').fn(t => { FP.f32.toInterval([kValue.f32.negative.subnormal.min, kValue.f32.negative.subnormal.max]), FP.f32.toInterval([kValue.f32.negative.infinity, kValue.f32.positive.infinity]), ]) { - const serialized = serializeFPInterval(interval); - const deserialized = deserializeFPInterval(serialized); + const s = new BinaryStream(new Uint8Array(1024)); + serializeFPInterval(s, interval); + const d = new BinaryStream(s.buffer()); + const deserialized = deserializeFPInterval(d); t.expect( objectEquals(interval, deserialized), `interval ${interval} -> serialize -> deserialize -> ${deserialized}` @@ -274,8 +280,10 @@ g.test('fpinterval_f16').fn(t => { FP.f16.toInterval([kValue.f16.negative.subnormal.min, kValue.f16.negative.subnormal.max]), FP.f16.toInterval([kValue.f16.negative.infinity, kValue.f16.positive.infinity]), ]) { - const serialized = serializeFPInterval(interval); - const deserialized = deserializeFPInterval(serialized); + const s = new BinaryStream(new Uint8Array(1024)); + serializeFPInterval(s, interval); + const d = new BinaryStream(s.buffer()); + const deserialized = deserializeFPInterval(d); t.expect( objectEquals(interval, deserialized), `interval ${interval} -> serialize -> deserialize -> ${deserialized}` @@ -308,8 +316,10 @@ g.test('fpinterval_abstract').fn(t => { FP.abstract.toInterval([kValue.f64.negative.subnormal.min, kValue.f64.negative.subnormal.max]), FP.abstract.toInterval([kValue.f64.negative.infinity, kValue.f64.positive.infinity]), ]) { - const serialized = serializeFPInterval(interval); - const deserialized = deserializeFPInterval(serialized); + const s = new BinaryStream(new Uint8Array(1024)); + serializeFPInterval(s, interval); + const d = new BinaryStream(s.buffer()); + const deserialized = deserializeFPInterval(d); t.expect( objectEquals(interval, deserialized), `interval ${interval} -> serialize -> deserialize -> ${deserialized}` @@ -328,8 +338,10 @@ g.test('expression_expectation').fn(t => { // Intervals [FP.f32.toInterval([-8.0, 0.5]), FP.f32.toInterval([2.0, 4.0])], ]) { - const serialized = serializeExpectation(expectation); - const deserialized = deserializeExpectation(serialized); + const s = new BinaryStream(new Uint8Array(1024)); + serializeExpectation(s, expectation); + const d = new BinaryStream(s.buffer()); + const deserialized = deserializeExpectation(d); t.expect( objectEquals(expectation, deserialized), `expectation ${expectation} -> serialize -> deserialize -> ${deserialized}` @@ -356,8 +368,10 @@ g.test('anyOf').fn(t => { testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)], }, ]) { - const serialized = serializeComparator(c.comparator); - const deserialized = deserializeComparator(serialized); + const s = new BinaryStream(new Uint8Array(1024)); + serializeComparator(s, c.comparator); + const d = new BinaryStream(s.buffer()); + const deserialized = deserializeComparator(d); for (const val of c.testCases) { const got = deserialized.compare(val); const expect = c.comparator.compare(val); @@ -382,8 +396,10 @@ g.test('skipUndefined').fn(t => { testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)], }, ]) { - const serialized = serializeComparator(c.comparator); - const deserialized = deserializeComparator(serialized); + const s = new BinaryStream(new Uint8Array(1024)); + serializeComparator(s, c.comparator); + const d = new BinaryStream(s.buffer()); + const deserialized = deserializeComparator(d); for (const val of c.testCases) { const got = deserialized.compare(val); const expect = c.comparator.compare(val); diff --git a/src/webgpu/shader/execution/expression/case_cache.ts b/src/webgpu/shader/execution/expression/case_cache.ts index e0aaa377192f..88f4a48df4c8 100644 --- a/src/webgpu/shader/execution/expression/case_cache.ts +++ b/src/webgpu/shader/execution/expression/case_cache.ts @@ -1,164 +1,125 @@ import { Cacheable, dataCache } from '../../../../common/framework/data_cache.js'; import { unreachable } from '../../../../common/util/util.js'; -import { - SerializedComparator, - deserializeComparator, - serializeComparator, -} from '../../../util/compare.js'; +import BinaryStream from '../../../util/binary_stream.js'; +import { deserializeComparator, serializeComparator } from '../../../util/compare.js'; import { Scalar, Vector, serializeValue, - SerializedValue, deserializeValue, Matrix, + Value, } from '../../../util/conversion.js'; import { deserializeFPInterval, FPInterval, - SerializedFPInterval, serializeFPInterval, } from '../../../util/floating_point.js'; import { flatten2DArray, unflatten2DArray } from '../../../util/math.js'; import { Case, CaseList, Expectation, isComparator } from './expression.js'; -/** - * SerializedExpectationValue holds the serialized form of an Expectation when - * the Expectation is a Value - * This form can be safely encoded to JSON. - */ -type SerializedExpectationValue = { - kind: 'value'; - value: SerializedValue; -}; - -/** - * SerializedExpectationInterval holds the serialized form of an Expectation when - * the Expectation is an Interval - * This form can be safely encoded to JSON. - */ -type SerializedExpectationInterval = { - kind: 'interval'; - value: SerializedFPInterval; -}; - -/** - * SerializedExpectationIntervals holds the serialized form of an Expectation when - * the Expectation is a list of Intervals - * This form can be safely encoded to JSON. - */ -type SerializedExpectationIntervals = { - kind: 'intervals'; - value: SerializedFPInterval[]; -}; - -/** - * SerializedExpectation2DIntervalArray holds the serialized form of an - * Expectation when the Expectation is a 2d array of Intervals. The array is - * flattened to a 1D array for storage. - * This form can be safely encoded to JSON. - */ -type SerializedExpectation2DIntervalArray = { - kind: '2d-interval-array'; - cols: number; - rows: number; - value: SerializedFPInterval[]; -}; - -/** - * SerializedExpectationValue holds the serialized form of an Expectation when - * the Expectation is a Comparator - * This form can be safely encoded to JSON. - */ -type SerializedExpectationComparator = { - kind: 'comparator'; - value: SerializedComparator; -}; - -/** - * SerializedExpectation holds the serialized form of an Expectation. - * This form can be safely encoded to JSON. - */ -export type SerializedExpectation = - | SerializedExpectationValue - | SerializedExpectationInterval - | SerializedExpectationIntervals - | SerializedExpectation2DIntervalArray - | SerializedExpectationComparator; +enum SerializedExpectationKind { + Value, + Interval, + Interval1DArray, + Interval2DArray, + Array, + Comparator, +} -/** serializeExpectation() converts an Expectation to a SerializedExpectation */ -export function serializeExpectation(e: Expectation): SerializedExpectation { +/** serializeExpectation() serializes an Expectation to a BinaryStream */ +export function serializeExpectation(s: BinaryStream, e: Expectation) { if (e instanceof Scalar || e instanceof Vector || e instanceof Matrix) { - return { kind: 'value', value: serializeValue(e) }; + s.writeU8(SerializedExpectationKind.Value); + serializeValue(s, e); + return; } if (e instanceof FPInterval) { - return { kind: 'interval', value: serializeFPInterval(e) }; + s.writeU8(SerializedExpectationKind.Interval); + serializeFPInterval(s, e); + return; } if (e instanceof Array) { if (e[0] instanceof Array) { e = e as FPInterval[][]; const cols = e.length; const rows = e[0].length; - return { - kind: '2d-interval-array', - cols, - rows, - value: flatten2DArray(e).map(serializeFPInterval), - }; + s.writeU8(SerializedExpectationKind.Interval2DArray); + s.writeU16(cols); + s.writeU16(rows); + s.writeArray(flatten2DArray(e), serializeFPInterval); } else { e = e as FPInterval[]; - return { kind: 'intervals', value: e.map(serializeFPInterval) }; + s.writeU8(SerializedExpectationKind.Interval1DArray); + s.writeArray(e, serializeFPInterval); } + return; } if (isComparator(e)) { - return { kind: 'comparator', value: serializeComparator(e) }; + s.writeU8(SerializedExpectationKind.Comparator); + serializeComparator(s, e); + return; } unreachable(`cannot serialize Expectation ${e}`); } -/** deserializeExpectation() converts a SerializedExpectation to a Expectation */ -export function deserializeExpectation(data: SerializedExpectation): Expectation { - switch (data.kind) { - case 'value': - return deserializeValue(data.value); - case 'interval': - return deserializeFPInterval(data.value); - case 'intervals': - return data.value.map(deserializeFPInterval); - case '2d-interval-array': - return unflatten2DArray(data.value.map(deserializeFPInterval), data.cols, data.rows); - case 'comparator': - return deserializeComparator(data.value); +/** deserializeExpectation() deserializes an Expectation from a BinaryStream */ +export function deserializeExpectation(s: BinaryStream): Expectation { + const kind = s.readU8(); + switch (kind) { + case SerializedExpectationKind.Value: { + return deserializeValue(s); + } + case SerializedExpectationKind.Interval: { + return deserializeFPInterval(s); + } + case SerializedExpectationKind.Interval1DArray: { + return s.readArray(deserializeFPInterval); + } + case SerializedExpectationKind.Interval2DArray: { + const cols = s.readU16(); + const rows = s.readU16(); + return unflatten2DArray(s.readArray(deserializeFPInterval), cols, rows); + } + case SerializedExpectationKind.Comparator: { + return deserializeComparator(s); + } + default: { + unreachable(`invalid serialized expectation kind: ${kind}`); + } } } -/** - * SerializedCase holds the serialized form of a Case. - * This form can be safely encoded to JSON. - */ -export type SerializedCase = { - input: SerializedValue | SerializedValue[]; - expected: SerializedExpectation; -}; - -/** serializeCase() converts an Case to a SerializedCase */ -export function serializeCase(c: Case): SerializedCase { - return { - input: c.input instanceof Array ? c.input.map(v => serializeValue(v)) : serializeValue(c.input), - expected: serializeExpectation(c.expected), - }; +/** serializeCase() serializes a Case to a BinaryStream */ +export function serializeCase(s: BinaryStream, c: Case) { + s.writeCond(c.input instanceof Array, { + if_true: () => { + // c.input is array + s.writeArray(c.input as Value[], serializeValue); + }, + if_false: () => { + // c.input is not array + serializeValue(s, c.input as Value); + }, + }); + serializeExpectation(s, c.expected); } -/** serializeCase() converts an SerializedCase to a Case */ -export function deserializeCase(data: SerializedCase): Case { - return { - input: - data.input instanceof Array - ? data.input.map(v => deserializeValue(v)) - : deserializeValue(data.input), - expected: deserializeExpectation(data.expected), - }; +/** deserializeCase() deserializes a Case from a BinaryStream */ +export function deserializeCase(s: BinaryStream): Case { + const input = s.readCond({ + if_true: () => { + // c.input is array + return s.readArray(deserializeValue); + }, + if_false: () => { + // c.input is not array + return deserializeValue(s); + }, + }); + const expected = deserializeExpectation(s); + return { input, expected }; } /** CaseListBuilder is a function that builds a CaseList */ @@ -176,7 +137,7 @@ export class CaseCache implements Cacheable> { * @param builders a Record of case-list name to case-list builder. */ constructor(name: string, builders: Record) { - this.path = `webgpu/shader/execution/case-cache/${name}.json`; + this.path = `webgpu/shader/execution/case-cache/${name}.bin`; this.builders = builders; } @@ -203,23 +164,28 @@ export class CaseCache implements Cacheable> { * serialize() implements the Cacheable.serialize interface. * @returns the serialized data. */ - serialize(data: Record): string { - const serialized: Record = {}; + serialize(data: Record): Uint8Array { + const maxSize = 32 << 20; // 32MB - max size for a file + const s = new BinaryStream(new Uint8Array(maxSize)); + s.writeU32(Object.keys(data).length); for (const name in data) { - serialized[name] = data[name].map(c => serializeCase(c)); + s.writeString(name); + s.writeArray(data[name], serializeCase); } - return JSON.stringify(serialized); + return s.buffer(); } /** * deserialize() implements the Cacheable.deserialize interface. * @returns the deserialize data. */ - deserialize(serialized: string): Record { - const data = JSON.parse(serialized) as Record; + deserialize(buffer: Uint8Array): Record { + const s = new BinaryStream(buffer); const casesByName: Record = {}; - for (const name in data) { - const cases = data[name].map(caseData => deserializeCase(caseData)); + const numRecords = s.readU32(); + for (let i = 0; i < numRecords; i++) { + const name = s.readString(); + const cases = s.readArray(deserializeCase); casesByName[name] = cases; } return casesByName; diff --git a/src/webgpu/util/binary_stream.ts b/src/webgpu/util/binary_stream.ts new file mode 100644 index 000000000000..1a17f4524d58 --- /dev/null +++ b/src/webgpu/util/binary_stream.ts @@ -0,0 +1,283 @@ +import { assert } from '../../common/util/util.js'; +import { Float16Array } from '../../external/petamoriken/float16/float16.js'; + +import { align } from './math.js'; + +/** + * BinaryStream is a utility to efficiently encode and decode numbers to / from a Uint8Array. + * BinaryStream uses a number of internal typed arrays to avoid small array allocations when reading + * and writing. + */ +export default class BinaryStream { + /** + * Constructor + * @param buffer the buffer to read from / write to. Array length must be a multiple of 8 bytes. + */ + constructor(buffer: Uint8Array) { + this.offset = 0; + this.u8 = buffer; + this.u16 = new Uint16Array(this.u8.buffer); + this.u32 = new Uint32Array(this.u8.buffer); + this.i8 = new Int8Array(this.u8.buffer); + this.i16 = new Int16Array(this.u8.buffer); + this.i32 = new Int32Array(this.u8.buffer); + this.f16 = new Float16Array(this.u8.buffer); + this.f32 = new Float32Array(this.u8.buffer); + this.f64 = new Float64Array(this.u8.buffer); + } + + /** buffer() returns the stream's buffer sliced to the 8-byte rounded read or write offset */ + buffer(): Uint8Array { + return this.u8.slice(0, align(this.offset, 8)); + } + + /** writeBool() writes a boolean as 255 or 0 to the buffer at the next byte offset */ + writeBool(value: boolean) { + this.u8[this.offset++] = value ? 255 : 0; + } + + /** readBool() reads a boolean from the buffer at the next byte offset */ + readBool(): boolean { + const val = this.u8[this.offset++]; + assert(val === 0 || val === 255); + return val !== 0; + } + + /** writeU8() writes a uint8 to the buffer at the next byte offset */ + writeU8(value: number) { + this.u8[this.offset++] = value; + } + + /** readU8() reads a uint8 from the buffer at the next byte offset */ + readU8(): number { + return this.u8[this.offset++]; + } + + /** u8View() returns a Uint8Array view of the uint8 at the next byte offset */ + u8View(): Uint8Array { + const at = this.offset++; + return new Uint8Array(this.u8.buffer, at, 1); + } + + /** writeU16() writes a uint16 to the buffer at the next 16-bit aligned offset */ + writeU16(value: number) { + this.u16[this.bumpWord(2)] = value; + } + + /** readU16() reads a uint16 from the buffer at the next 16-bit aligned offset */ + readU16(): number { + return this.u16[this.bumpWord(2)]; + } + + /** u16View() returns a Uint16Array view of the uint16 at the next 16-bit aligned offset */ + u16View(): Uint16Array { + const at = this.bumpWord(2); + return new Uint16Array(this.u16.buffer, at * 2, 1); + } + + /** writeU32() writes a uint32 to the buffer at the next 32-bit aligned offset */ + writeU32(value: number) { + this.u32[this.bumpWord(4)] = value; + } + + /** readU32() reads a uint32 from the buffer at the next 32-bit aligned offset */ + readU32(): number { + return this.u32[this.bumpWord(4)]; + } + + /** u32View() returns a Uint32Array view of the uint32 at the next 32-bit aligned offset */ + u32View(): Uint32Array { + const at = this.bumpWord(4); + return new Uint32Array(this.u32.buffer, at * 4, 1); + } + + /** writeI8() writes a int8 to the buffer at the next byte offset */ + writeI8(value: number) { + this.i8[this.offset++] = value; + } + + /** readI8() reads a int8 from the buffer at the next byte offset */ + readI8(): number { + return this.i8[this.offset++]; + } + + /** i8View() returns a Uint8Array view of the uint8 at the next byte offset */ + i8View(): Int8Array { + const at = this.offset++; + return new Int8Array(this.i8.buffer, at, 1); + } + + /** writeI16() writes a int16 to the buffer at the next 16-bit aligned offset */ + writeI16(value: number) { + this.i16[this.bumpWord(2)] = value; + } + + /** readI16() reads a int16 from the buffer at the next 16-bit aligned offset */ + readI16(): number { + return this.i16[this.bumpWord(2)]; + } + + /** i16View() returns a Int16Array view of the uint16 at the next 16-bit aligned offset */ + i16View(): Int16Array { + const at = this.bumpWord(2); + return new Int16Array(this.i16.buffer, at * 2, 1); + } + + /** writeI32() writes a int32 to the buffer at the next 32-bit aligned offset */ + writeI32(value: number) { + this.i32[this.bumpWord(4)] = value; + } + + /** readI32() reads a int32 from the buffer at the next 32-bit aligned offset */ + readI32(): number { + return this.i32[this.bumpWord(4)]; + } + + /** i32View() returns a Int32Array view of the uint32 at the next 32-bit aligned offset */ + i32View(): Int32Array { + const at = this.bumpWord(4); + return new Int32Array(this.i32.buffer, at * 4, 1); + } + + /** writeF16() writes a float16 to the buffer at the next 16-bit aligned offset */ + writeF16(value: number) { + this.f16[this.bumpWord(2)] = value; + } + + /** readF16() reads a float16 from the buffer at the next 16-bit aligned offset */ + readF16(): number { + return this.f16[this.bumpWord(2)]; + } + + /** f16View() returns a Float16Array view of the uint16 at the next 16-bit aligned offset */ + f16View(): Float16Array { + const at = this.bumpWord(2); + return new Float16Array(this.f16.buffer, at * 2, 1); + } + + /** writeF32() writes a float32 to the buffer at the next 32-bit aligned offset */ + writeF32(value: number) { + this.f32[this.bumpWord(4)] = value; + } + + /** readF32() reads a float32 from the buffer at the next 32-bit aligned offset */ + readF32(): number { + return this.f32[this.bumpWord(4)]; + } + + /** f32View() returns a Float32Array view of the uint32 at the next 32-bit aligned offset */ + f32View(): Float32Array { + const at = this.bumpWord(4); + return new Float32Array(this.f32.buffer, at * 4, 1); + } + + /** writeF64() writes a float64 to the buffer at the next 64-bit aligned offset */ + writeF64(value: number) { + this.f64[this.bumpWord(8)] = value; + } + + /** readF64() reads a float64 from the buffer at the next 64-bit aligned offset */ + readF64(): number { + return this.f64[this.bumpWord(8)]; + } + + /** f64View() returns a Float64Array view of the uint64 at the next 64-bit aligned offset */ + f64View(): Float64Array { + const at = this.bumpWord(8); + return new Float64Array(this.f64.buffer, at * 8, 1); + } + + /** + * writeString() writes a length-prefixed UTF-16 string to the buffer at the next 32-bit aligned + * offset + */ + writeString(value: string) { + this.writeU32(value.length); + for (let i = 0; i < value.length; i++) { + this.writeU16(value.charCodeAt(i)); + } + } + + /** + * readString() writes a length-prefixed UTF-16 string from the buffer at the next 32-bit aligned + * offset + */ + readString(): string { + const len = this.readU32(); + const codes = new Array(len); + for (let i = 0; i < len; i++) { + codes[i] = this.readU16(); + } + return String.fromCharCode(...codes); + } + + /** + * writeArray() writes a length-prefixed array of T elements to the buffer at the next 32-bit + * aligned offset, using the provided callback to write the individual elements + */ + writeArray(value: readonly T[], writeElement: (s: BinaryStream, element: T) => void) { + this.writeU32(value.length); + for (const element of value) { + writeElement(this, element); + } + } + + /** + * readArray() reads a length-prefixed array of T elements from the buffer at the next 32-bit + * aligned offset, using the provided callback to read the individual elements + */ + readArray(readElement: (s: BinaryStream) => T): T[] { + const len = this.readU32(); + const array = new Array(len); + for (let i = 0; i < len; i++) { + array[i] = readElement(this); + } + return array; + } + + /** + * writeCond() writes the boolean condition @p cond to the buffer, then either calls if_true if + * @p cond is true, otherwise if_false + */ + writeCond(cond: boolean, fns: { if_true: () => T; if_false: () => F }) { + this.writeBool(cond); + if (cond) { + return fns.if_true(); + } else { + return fns.if_false(); + } + } + + /** + * readCond() reads a boolean condition from the buffer, then either calls if_true if + * the condition was is true, otherwise if_false + */ + readCond(fns: { if_true: () => T; if_false: () => F }) { + if (this.readBool()) { + return fns.if_true(); + } else { + return fns.if_false(); + } + } + + /** + * bumpWord() increments this.offset by @p bytes, after first aligning this.offset to @p bytes. + * @returns the old offset aligned to the next multiple of @p bytes, divided by @p bytes. + */ + private bumpWord(bytes: number) { + const multiple = Math.floor((this.offset + bytes - 1) / bytes); + this.offset = (multiple + 1) * bytes; + return multiple; + } + + private offset: number; + private u8: Uint8Array; + private u16: Uint16Array; + private u32: Uint32Array; + private i8: Int8Array; + private i16: Int16Array; + private i32: Int32Array; + private f16: Float16Array; + private f32: Float32Array; + private f64: Float64Array; +} diff --git a/src/webgpu/util/compare.ts b/src/webgpu/util/compare.ts index 6fe7b34466d7..45599d25f63c 100644 --- a/src/webgpu/util/compare.ts +++ b/src/webgpu/util/compare.ts @@ -3,11 +3,11 @@ import { Colors } from '../../common/util/colors.js'; import { assert, unreachable } from '../../common/util/util.js'; import { deserializeExpectation, - SerializedExpectation, serializeExpectation, } from '../shader/execution/expression/case_cache.js'; import { Expectation, toComparator } from '../shader/execution/expression/expression.js'; +import BinaryStream from './binary_stream.js'; import { isFloatValue, Matrix, Scalar, Value, Vector } from './conversion.js'; import { FPInterval } from './floating_point.js'; @@ -40,6 +40,40 @@ export interface Comparator { data?: Expectation | Expectation[] | string; } +/** SerializedComparator is an enum of all the possible serialized comparator types. */ +enum SerializedComparatorKind { + AnyOf, + SkipUndefined, + AlwaysPass, +} + +/** serializeComparatorKind() serializes a ComparatorKind to a BinaryStream */ +function serializeComparatorKind(s: BinaryStream, value: ComparatorKind) { + switch (value) { + case 'anyOf': + return s.writeU8(SerializedComparatorKind.AnyOf); + case 'skipUndefined': + return s.writeU8(SerializedComparatorKind.SkipUndefined); + case 'alwaysPass': + return s.writeU8(SerializedComparatorKind.AlwaysPass); + } +} + +/** deserializeComparatorKind() deserializes a ComparatorKind from a BinaryStream */ +function deserializeComparatorKind(s: BinaryStream): ComparatorKind { + const kind = s.readU8(); + switch (kind) { + case SerializedComparatorKind.AnyOf: + return 'anyOf'; + case SerializedComparatorKind.SkipUndefined: + return 'skipUndefined'; + case SerializedComparatorKind.AlwaysPass: + return 'alwaysPass'; + default: + unreachable(`invalid serialized ComparatorKind: ${kind}`); + } +} + /** * compares 'got' Value to 'expected' Value, returning the Comparison information. * @param got the Value obtained from the test @@ -383,54 +417,27 @@ export function alwaysPass(msg: string = 'always pass'): Comparator { return c; } -/** SerializedComparatorAnyOf is the serialized type of `anyOf` comparator. */ -type SerializedComparatorAnyOf = { - kind: 'anyOf'; - data: SerializedExpectation[]; -}; - -/** SerializedComparatorSkipUndefined is the serialized type of `skipUndefined` comparator. */ -type SerializedComparatorSkipUndefined = { - kind: 'skipUndefined'; - data?: SerializedExpectation; -}; - -/** SerializedComparatorAlwaysPass is the serialized type of `alwaysPass` comparator. */ -type SerializedComparatorAlwaysPass = { - kind: 'alwaysPass'; - reason: string; -}; - -// Serialized forms of 'value' and 'packed' are intentionally omitted, so should -// not be put into the cache. Attempting to will cause a runtime assert. - -/** SerializedComparator is a union of all the possible serialized comparator types. */ -export type SerializedComparator = - | SerializedComparatorAnyOf - | SerializedComparatorSkipUndefined - | SerializedComparatorAlwaysPass; - -/** - * Serializes a Comparator to a SerializedComparator. - * @param c the Comparator - * @returns a serialized comparator - */ -export function serializeComparator(c: Comparator): SerializedComparator { +/** serializeComparator() serializes a Comparator to a BinaryStream */ +export function serializeComparator(s: BinaryStream, c: Comparator) { + serializeComparatorKind(s, c.kind); switch (c.kind) { - case 'anyOf': { - const d = c.data as Expectation[]; - return { kind: 'anyOf', data: d.map(serializeExpectation) }; - } - case 'skipUndefined': { - if (c.data !== undefined) { - const d = c.data as Expectation; - return { kind: 'skipUndefined', data: serializeExpectation(d) }; - } - return { kind: 'skipUndefined', data: undefined }; - } + case 'anyOf': + s.writeArray(c.data as Expectation[], serializeExpectation); + return; + case 'skipUndefined': + s.writeCond(c.data !== undefined, { + if_true: () => { + // defined data + serializeExpectation(s, c.data as Expectation); + }, + if_false: () => { + // undefined data + }, + }); + return; case 'alwaysPass': { - const d = c.data as string; - return { kind: 'alwaysPass', reason: d }; + s.writeString(c.data as string); + return; } case 'value': case 'packed': { @@ -441,22 +448,25 @@ export function serializeComparator(c: Comparator): SerializedComparator { unreachable(`Unable serialize comparator '${c}'`); } -/** - * Deserializes a Comparator from a SerializedComparator. - * @param s the SerializedComparator - * @returns the deserialized comparator. - */ -export function deserializeComparator(s: SerializedComparator): Comparator { - switch (s.kind) { - case 'anyOf': { - return anyOf(...s.data.map(e => deserializeExpectation(e))); - } - case 'skipUndefined': { - return skipUndefined(s.data !== undefined ? deserializeExpectation(s.data) : undefined); - } - case 'alwaysPass': { - return alwaysPass(s.reason); - } +/** deserializeComparator() deserializes a Comparator from a BinaryStream */ +export function deserializeComparator(s: BinaryStream): Comparator { + const kind = deserializeComparatorKind(s); + switch (kind) { + case 'anyOf': + return anyOf(...s.readArray(deserializeExpectation)); + case 'skipUndefined': + return s.readCond({ + if_true: () => { + // defined data + return skipUndefined(deserializeExpectation(s)); + }, + if_false: () => { + // undefined data + return skipUndefined(undefined); + }, + }); + case 'alwaysPass': + return alwaysPass(s.readString()); } unreachable(`Unable deserialize comparator '${s}'`); } diff --git a/src/webgpu/util/conversion.ts b/src/webgpu/util/conversion.ts index e78af9783288..28a6e78f9137 100644 --- a/src/webgpu/util/conversion.ts +++ b/src/webgpu/util/conversion.ts @@ -3,6 +3,7 @@ import { ROArrayArray } from '../../common/util/types.js'; import { assert, objectEquals, TypedArrayBufferView, unreachable } from '../../common/util/util.js'; import { Float16Array } from '../../external/petamoriken/float16/float16.js'; +import BinaryStream from './binary_stream.js'; import { kBit } from './constants.js'; import { cartesianProduct, @@ -888,11 +889,11 @@ export class Scalar { } /** - * Copies the scalar value to the Uint8Array buffer at the provided byte offset. + * Copies the scalar value to the buffer at the provided byte offset. * @param buffer the destination buffer - * @param offset the byte offset within buffer + * @param offset the offset in buffer, in units of @p buffer */ - public copyTo(buffer: Uint8Array, offset: number) { + public copyTo(buffer: TypedArrayBufferView, offset: number) { assert(this.type.kind !== 'f64', `Copying f64 values to/from buffers is not defined`); workingDataU32[1] = this.bits1; workingDataU32[0] = this.bits0; @@ -1301,86 +1302,222 @@ export type SerializedValueMatrix = { value: ROArrayArray; }; -export type SerializedValue = SerializedValueScalar | SerializedValueVector | SerializedValueMatrix; +enum SerializedScalarKind { + AbstractFloat, + F64, + F32, + F16, + U32, + U16, + U8, + I32, + I16, + I8, + Bool, +} -export function serializeValue(v: Value): SerializedValue { - const value = (kind: ScalarKind, s: Scalar) => { +/** serializeScalarKind() serializes a ScalarKind to a BinaryStream */ +function serializeScalarKind(s: BinaryStream, v: ScalarKind) { + switch (v) { + case 'abstract-float': + s.writeU8(SerializedScalarKind.AbstractFloat); + return; + case 'f64': + s.writeU8(SerializedScalarKind.F64); + return; + case 'f32': + s.writeU8(SerializedScalarKind.F32); + return; + case 'f16': + s.writeU8(SerializedScalarKind.F16); + return; + case 'u32': + s.writeU8(SerializedScalarKind.U32); + return; + case 'u16': + s.writeU8(SerializedScalarKind.U16); + return; + case 'u8': + s.writeU8(SerializedScalarKind.U8); + return; + case 'i32': + s.writeU8(SerializedScalarKind.I32); + return; + case 'i16': + s.writeU8(SerializedScalarKind.I16); + return; + case 'i8': + s.writeU8(SerializedScalarKind.I8); + return; + case 'bool': + s.writeU8(SerializedScalarKind.Bool); + return; + } +} + +/** deserializeScalarKind() deserializes a ScalarKind from a BinaryStream */ +function deserializeScalarKind(s: BinaryStream): ScalarKind { + const kind = s.readU8(); + switch (kind) { + case SerializedScalarKind.AbstractFloat: + return 'abstract-float'; + case SerializedScalarKind.F64: + return 'f64'; + case SerializedScalarKind.F32: + return 'f32'; + case SerializedScalarKind.F16: + return 'f16'; + case SerializedScalarKind.U32: + return 'u32'; + case SerializedScalarKind.U16: + return 'u16'; + case SerializedScalarKind.U8: + return 'u8'; + case SerializedScalarKind.I32: + return 'i32'; + case SerializedScalarKind.I16: + return 'i16'; + case SerializedScalarKind.I8: + return 'i8'; + case SerializedScalarKind.Bool: + return 'bool'; + default: + unreachable(`invalid serialized ScalarKind: ${kind}`); + } +} + +enum SerializedValueKind { + Scalar, + Vector, + Matrix, +} + +/** serializeValue() serializes a Value to a BinaryStream */ +export function serializeValue(s: BinaryStream, v: Value) { + const serializeScalar = (scalar: Scalar, kind: ScalarKind) => { switch (kind) { + case 'abstract-float': + s.writeF64(scalar.value as number); + return; + case 'f64': + s.writeF64(scalar.value as number); + return; case 'f32': - return s.bits0; + s.writeF32(scalar.value as number); + return; case 'f16': - return s.bits0; - default: - return s.value; + s.writeF16(scalar.value as number); + return; + case 'u32': + s.writeU32(scalar.value as number); + return; + case 'u16': + s.writeU16(scalar.value as number); + return; + case 'u8': + s.writeU8(scalar.value as number); + return; + case 'i32': + s.writeI32(scalar.value as number); + return; + case 'i16': + s.writeI16(scalar.value as number); + return; + case 'i8': + s.writeI8(scalar.value as number); + return; + case 'bool': + s.writeBool(scalar.value as boolean); + return; } }; + if (v instanceof Scalar) { - const kind = v.type.kind; - return { - kind: 'scalar', - type: kind, - value: value(kind, v), - }; + s.writeU8(SerializedValueKind.Scalar); + serializeScalarKind(s, v.type.kind); + serializeScalar(v, v.type.kind); + return; } if (v instanceof Vector) { - const kind = v.type.elementType.kind; - return { - kind: 'vector', - type: kind, - value: v.elements.map(e => value(kind, e)) as boolean[] | readonly number[], - }; + s.writeU8(SerializedValueKind.Vector); + serializeScalarKind(s, v.type.elementType.kind); + s.writeU8(v.type.width); + for (const element of v.elements) { + serializeScalar(element, v.type.elementType.kind); + } + return; } if (v instanceof Matrix) { - const kind = v.type.elementType.kind; - return { - kind: 'matrix', - type: kind, - value: v.elements.map(c => c.map(r => value(kind, r))) as ROArrayArray, - }; + s.writeU8(SerializedValueKind.Matrix); + serializeScalarKind(s, v.type.elementType.kind); + s.writeU8(v.type.cols); + s.writeU8(v.type.rows); + for (const column of v.elements) { + for (const element of column) { + serializeScalar(element, v.type.elementType.kind); + } + } + return; } unreachable(`unhandled value type: ${v}`); } -export function deserializeValue(data: SerializedValue): Value { - const buildScalar = (v: ScalarValue): Scalar => { - switch (data.type) { +/** deserializeValue() deserializes a Value from a BinaryStream */ +export function deserializeValue(s: BinaryStream): Value { + const deserializeScalar = (kind: ScalarKind) => { + switch (kind) { case 'abstract-float': - return abstractFloat(v as number); + return abstractFloat(s.readF64()); case 'f64': - return f64(v as number); - case 'i32': - return i32(v as number); - case 'u32': - return u32(v as number); + return f64(s.readF64()); case 'f32': - return f32Bits(v as number); - case 'i16': - return i16(v as number); - case 'u16': - return u16(v as number); + return f32(s.readF32()); case 'f16': - return f16Bits(v as number); - case 'i8': - return i8(v as number); + return f16(s.readF16()); + case 'u32': + return u32(s.readU32()); + case 'u16': + return u16(s.readU16()); case 'u8': - return u8(v as number); + return u8(s.readU8()); + case 'i32': + return i32(s.readI32()); + case 'i16': + return i16(s.readI16()); + case 'i8': + return i8(s.readI8()); case 'bool': - return bool(v as boolean); - default: - unreachable(`unhandled value type: ${data.type}`); + return bool(s.readBool()); } }; - switch (data.kind) { - case 'scalar': { - return buildScalar(data.value); - } - case 'vector': { - return new Vector(data.value.map(v => buildScalar(v))); + const valueKind = s.readU8(); + const scalarKind = deserializeScalarKind(s); + switch (valueKind) { + case SerializedValueKind.Scalar: + return deserializeScalar(scalarKind); + case SerializedValueKind.Vector: { + const width = s.readU8(); + const scalars = new Array(width); + for (let i = 0; i < width; i++) { + scalars[i] = deserializeScalar(scalarKind); + } + return new Vector(scalars); } - case 'matrix': { - return new Matrix(data.value.map(c => c.map(buildScalar))); + case SerializedValueKind.Matrix: { + const numCols = s.readU8(); + const numRows = s.readU8(); + const columns = new Array(numCols); + for (let c = 0; c < numCols; c++) { + columns[c] = new Array(numRows); + for (let i = 0; i < numRows; i++) { + columns[c][i] = deserializeScalar(scalarKind); + } + } + return new Matrix(columns); } + default: + unreachable(`invalid serialized value kind: ${valueKind}`); } } diff --git a/src/webgpu/util/floating_point.ts b/src/webgpu/util/floating_point.ts index 18a640f43403..b13f20fd6653 100644 --- a/src/webgpu/util/floating_point.ts +++ b/src/webgpu/util/floating_point.ts @@ -3,6 +3,7 @@ import { assert, unreachable } from '../../common/util/util.js'; import { Float16Array } from '../../external/petamoriken/float16/float16.js'; import { Case, IntervalFilter } from '../shader/execution/expression/expression.js'; +import BinaryStream from './binary_stream.js'; import { anyOf } from './compare.js'; import { kValue } from './constants.js'; import { @@ -40,18 +41,45 @@ import { unflatten2DArray, every2DArray, } from './math.js'; -import { - reinterpretF16AsU16, - reinterpretF32AsU32, - reinterpretF64AsU32s, - reinterpretU16AsF16, - reinterpretU32AsF32, - reinterpretU32sAsF64, -} from './reinterpret.js'; /** Indicate the kind of WGSL floating point numbers being operated on */ export type FPKind = 'f32' | 'f16' | 'abstract'; +enum SerializedFPIntervalKind { + Abstract, + F32, + F16, +} + +/** serializeFPKind() serializes a FPKind to a BinaryStream */ +export function serializeFPKind(s: BinaryStream, value: FPKind) { + switch (value) { + case 'abstract': + s.writeU8(SerializedFPIntervalKind.Abstract); + break; + case 'f16': + s.writeU8(SerializedFPIntervalKind.F16); + break; + case 'f32': + s.writeU8(SerializedFPIntervalKind.F32); + break; + } +} + +/** deserializeFPKind() deserializes a FPKind from a BinaryStream */ +export function deserializeFPKind(s: BinaryStream): FPKind { + const kind = s.readU8(); + switch (kind) { + case SerializedFPIntervalKind.Abstract: + return 'abstract'; + case SerializedFPIntervalKind.F16: + return 'f16'; + case SerializedFPIntervalKind.F32: + return 'f32'; + default: + unreachable(`invalid deserialized FPKind: ${kind}`); + } +} // Containers /** @@ -138,81 +166,59 @@ export class FPInterval { } } -/** - * SerializedFPInterval holds the serialized form of a FPInterval. - * This form can be safely encoded to JSON. - */ -export type SerializedFPInterval = - | { kind: 'f32'; unbounded: false; begin: number; end: number } - | { kind: 'f32'; unbounded: true } - | { kind: 'f16'; unbounded: false; begin: number; end: number } - | { kind: 'f16'; unbounded: true } - | { kind: 'abstract'; unbounded: false; begin: [number, number]; end: [number, number] } - | { kind: 'abstract'; unbounded: true }; - -/** serializeFPInterval() converts a FPInterval to a SerializedFPInterval */ -export function serializeFPInterval(i: FPInterval): SerializedFPInterval { +/** serializeFPInterval() serializes a FPInterval to a BinaryStream */ +export function serializeFPInterval(s: BinaryStream, i: FPInterval) { + serializeFPKind(s, i.kind); const traits = FP[i.kind]; - switch (i.kind) { - case 'abstract': { - if (i === traits.constants().unboundedInterval) { - return { kind: 'abstract', unbounded: true }; - } else { - return { - kind: 'abstract', - unbounded: false, - begin: reinterpretF64AsU32s(i.begin), - end: reinterpretF64AsU32s(i.end), - }; - } - } - case 'f32': { - if (i === traits.constants().unboundedInterval) { - return { kind: 'f32', unbounded: true }; - } else { - return { - kind: 'f32', - unbounded: false, - begin: reinterpretF32AsU32(i.begin), - end: reinterpretF32AsU32(i.end), - }; + s.writeCond(i !== traits.constants().unboundedInterval, { + if_true: () => { + // Bounded + switch (i.kind) { + case 'abstract': + s.writeF64(i.begin); + s.writeF64(i.end); + break; + case 'f32': + s.writeF32(i.begin); + s.writeF32(i.end); + break; + case 'f16': + s.writeF16(i.begin); + s.writeF16(i.end); + break; + default: + unreachable(`Unable to serialize FPInterval ${i}`); + break; } - } - case 'f16': { - if (i === traits.constants().unboundedInterval) { - return { kind: 'f16', unbounded: true }; - } else { - return { - kind: 'f16', - unbounded: false, - begin: reinterpretF16AsU16(i.begin), - end: reinterpretF16AsU16(i.end), - }; - } - } - } - unreachable(`Unable to serialize FPInterval ${i}`); + }, + if_false: () => { + // Unbounded + }, + }); } -/** serializeFPInterval() converts a SerializedFPInterval to a FPInterval */ -export function deserializeFPInterval(data: SerializedFPInterval): FPInterval { - const kind = data.kind; +/** deserializeFPInterval() deserializes a FPInterval from a BinaryStream */ +export function deserializeFPInterval(s: BinaryStream): FPInterval { + const kind = deserializeFPKind(s); const traits = FP[kind]; - if (data.unbounded) { - return traits.constants().unboundedInterval; - } - switch (kind) { - case 'abstract': { - return traits.toInterval([reinterpretU32sAsF64(data.begin), reinterpretU32sAsF64(data.end)]); - } - case 'f32': { - return traits.toInterval([reinterpretU32AsF32(data.begin), reinterpretU32AsF32(data.end)]); - } - case 'f16': { - return traits.toInterval([reinterpretU16AsF16(data.begin), reinterpretU16AsF16(data.end)]); - } - } - unreachable(`Unable to deserialize data ${data}`); + return s.readCond({ + if_true: () => { + // Bounded + switch (kind) { + case 'abstract': + return traits.toInterval([s.readF64(), s.readF64()]); + case 'f32': + return traits.toInterval([s.readF32(), s.readF32()]); + case 'f16': + return traits.toInterval([s.readF16(), s.readF16()]); + } + unreachable(`Unable to deserialize FPInterval with kind ${kind}`); + }, + if_false: () => { + // Unbounded + return traits.constants().unboundedInterval; + }, + }); } /**