diff --git a/package.json b/package.json index 44367a17..00930cec 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "license": "MIT", "dependencies": { "content-type": "^1.0.4", + "gl-matrix": "^3.3.0", "uWebSockets.js": "github:uNetworking/uWebSockets.js#v18.3.0", "ws": "^7.3.1" }, diff --git a/src/schema/index.ts b/src/schema/index.ts index 6ffcec4f..684b836e 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -23,7 +23,6 @@ import { MuUint16 } from './uint16'; import { MuUint32 } from './uint32'; import { MuVarint } from './varint'; import { MuRelativeVarint } from './rvarint'; -import { MuQuantizedFloat } from './quantized-float'; // functors import { MuArray } from './array'; @@ -37,6 +36,25 @@ import { MuBytes } from './bytes'; import { MuDictionary } from './dictionary'; import { MuVector } from './vector'; +// math +import { + MuVec2, + MuVec3, + MuVec4, + MuQuat, + MuMat2, + MuMat3, + MuMat4, +} from './math'; + +// quantized +import { MuQuantizedFloat } from './quantized-float'; +import { + MuQuantizedVec2, + MuQuantizedVec3, + MuQuantizedVec4, +} from './quantized-vector'; + // misc. types import { MuDate } from './date'; import { MuJSON } from './json'; @@ -59,7 +77,6 @@ export { MuUint32, MuVarint, MuRelativeVarint, - MuQuantizedFloat, MuArray, MuOption, @@ -71,6 +88,19 @@ export { MuVector, MuDictionary, + MuVec2, + MuVec3, + MuVec4, + MuQuat, + MuMat2, + MuMat3, + MuMat4, + + MuQuantizedFloat, + MuQuantizedVec2, + MuQuantizedVec3, + MuQuantizedVec4, + MuDate, MuJSON, }; diff --git a/src/schema/math.ts b/src/schema/math.ts new file mode 100644 index 00000000..45ab72bd --- /dev/null +++ b/src/schema/math.ts @@ -0,0 +1,173 @@ +import { vec2, vec3, vec4, quat, mat2, mat3, mat4 } from 'gl-matrix'; +import { MuFloat32, MuVector, MuSchema } from './index'; +import { MuWriteStream, MuReadStream } from '../stream'; + +export const MuVec2:MuSchema = new MuVector(new MuFloat32(), 2); + +const vec3Pool:vec3[] = []; +export const MuVec3:MuSchema = { + muType: 'vector', + identity: vec3.create(), + json: { + type: 'vector', + data: [0, 0, 0], + }, + alloc: () => { + return vec3Pool.pop() || vec3.create(); + }, + free: (v:vec3) => { + vec3Pool.push(v); + }, + assign: (dst:vec3, src:vec3) => { + dst[0] = src[0]; + dst[1] = src[1]; + dst[2] = src[2]; + return dst; + }, + equal: (dst:vec3, src:vec3) => { + return ( + dst[0] === src[0] && + dst[1] === src[1] && + dst[2] === src[2] + ); + }, + diff: (base:vec3, target:vec3, out:MuWriteStream) => { + const s0 = (base[0] !== target[0]) << 0; + const s1 = (base[1] !== target[1]) << 1; + const s2 = (base[2] !== target[2]) << 2; + const flags = s0 + s1 + s2; + if (flags === 0) { + return false; + } + out.grow(13); + out.writeUint8(flags); + if (s0) { out.writeFloat32(target[0]); } + if (s1) { out.writeFloat32(target[1]); } + if (s2) { out.writeFloat32(target[2]); } + return true; + }, + patch: (base:vec3, inp:MuReadStream) => { + const v = vec3Pool.pop() || vec3.create(); + const flags = inp.readUint8(); + v[0] = (flags & 1) ? inp.readFloat32() : base[0]; + v[1] = (flags & 2) ? inp.readFloat32() : base[1]; + v[2] = (flags & 4) ? inp.readFloat32() : base[2]; + return v; + }, + clone: (v:vec3) => { + const x = vec3Pool.pop(); + if (x) { + x[0] = v[0]; + x[1] = v[1]; + x[2] = v[2]; + return x; + } + return vec3.clone(v); + }, + toJSON: (v:vec3) => [ v[0], v[1], v[2] ], + fromJSON: (json:any) => { + let v = vec3Pool.pop(); + if (Array.isArray(json)) { + v = v || vec3.create(); + v[0] = +json[0] || 0; + v[1] = +json[1] || 0; + v[2] = +json[2] || 0; + return v; + } else if (v) { + v[0] = v[1] = v[2] = 0; + return v; + } else { + return vec3.create(); + } + }, +}; + +const vec4Pool:vec4[] = []; +export const MuVec4:MuSchema = { + muType: 'vector', + identity: vec4.create(), + json: { + type: 'vector', + data: [0, 0, 0, 0], + }, + alloc: () => { + return vec4Pool.pop() || vec4.create(); + }, + free: (v:vec4) => { + vec4Pool.push(v); + }, + assign: (dst:vec4, src:vec4) => { + dst[0] = src[0]; + dst[1] = src[1]; + dst[2] = src[2]; + dst[3] = src[3]; + return dst; + }, + equal: (dst:vec4, src:vec4) => { + return ( + dst[0] === src[0] && + dst[1] === src[1] && + dst[2] === src[2] && + dst[3] === src[3] + ); + }, + diff: (base:vec4, target:vec4, out:MuWriteStream) => { + const s0 = (base[0] !== target[0]) << 0; + const s1 = (base[1] !== target[1]) << 1; + const s2 = (base[2] !== target[2]) << 2; + const s3 = (base[3] !== target[3]) << 3; + const flags = s0 + s1 + s2 + s3; + if (flags === 0) { + return false; + } + out.grow(17); + out.writeUint8(flags); + if (s0) { out.writeFloat32(target[0]); } + if (s1) { out.writeFloat32(target[1]); } + if (s2) { out.writeFloat32(target[2]); } + if (s3) { out.writeFloat32(target[3]); } + return true; + }, + patch: (base:vec4, inp:MuReadStream) => { + const v = vec4Pool.pop() || vec4.create(); + const flags = inp.readUint8(); + v[0] = (flags & 1) ? inp.readFloat32() : base[0]; + v[1] = (flags & 2) ? inp.readFloat32() : base[1]; + v[2] = (flags & 4) ? inp.readFloat32() : base[2]; + v[3] = (flags & 8) ? inp.readFloat32() : base[3]; + return v; + }, + clone: (v:vec4) => { + const x = vec4Pool.pop(); + if (x) { + x[0] = v[0]; + x[1] = v[1]; + x[2] = v[2]; + x[3] = v[3]; + return x; + } + return vec4.clone(v); + }, + toJSON: (v:vec4) => [ v[0], v[1], v[2], v[3] ], + fromJSON: (json:any) => { + let v = vec4Pool.pop(); + if (Array.isArray(json)) { + v = v || vec4.create(); + v[0] = +json[0] || 0; + v[1] = +json[1] || 0; + v[2] = +json[2] || 0; + v[3] = +json[3] || 0; + return v; + } else if (v) { + v[0] = v[1] = v[2] = v[3] = 0; + return v; + } else { + return vec4.create(); + } + }, +}; + +export const MuQuat:MuSchema = MuVec4; +export const MuMat2:MuSchema = MuVec4; +export const MuMat3:MuSchema = new MuVector(new MuFloat32(), 9); +export const MuMat4:MuSchema = new MuVector(new MuFloat32(), 16); diff --git a/src/schema/quantized-vector.ts b/src/schema/quantized-vector.ts new file mode 100644 index 00000000..748c7818 --- /dev/null +++ b/src/schema/quantized-vector.ts @@ -0,0 +1,517 @@ +import { vec2, vec3, vec4 } from 'gl-matrix'; +import { MuWriteStream, MuReadStream } from '../stream'; +import { MuSchema } from './schema'; +import { MuVec2, MuVec3, MuVec4 } from './math'; + +const SCHROEPPEL2 = 0xAAAAAAAA; + +function readShroeppel (stream:MuReadStream) { + const x = stream.readVarint(); + return ((SCHROEPPEL2 ^ x) - SCHROEPPEL2) >> 0; +} + +function writeVarIntWithPrefix2 (code:number, x:number, stream:MuWriteStream) { + const x0 = x & 31; + const x1 = x >>> 5; + const prefix = code | (x1 ? 4 : 0); + stream.writeUint8((prefix << 5) | x0); + if (x1) { + stream.writeVarint(x1); + } +} + +function readShroeppelWithPrefix2 (prefix:number, stream:MuReadStream) { + let x = prefix & 31; + if (prefix & 128) { + x += stream.readVarint() << 5; + } + return (SCHROEPPEL2 ^ x) - SCHROEPPEL2 >> 0; +} + +export class MuQuantizedVec2 implements MuSchema { + public invPrecision = 1; + public identity = vec2.create(); + public json:{ + type:'quantized-vec2'; + precision:number; + identity:number[]; + }; + public muData:{ + type:'quantized-vec2'; + precision:number; + identity:number[]; + } = { + type: 'quantized-vec2', + precision: 0, + identity: [0, 0], + }; + public readonly muType = 'quantized-vec2'; + + constructor ( + public precision:number, + identity?:vec2) { + this.invPrecision = 1 / this.precision; + if (identity) { + this.identity[0] = this.precision * (Math.round(this.invPrecision * identity[0]) >> 0); + this.identity[1] = this.precision * (Math.round(this.invPrecision * identity[1]) >> 0); + } + this.json = this.muData = { + type: 'quantized-vec2', + precision: this.precision, + identity: [this.identity[0], this.identity[1]], + }; + } + + public assign(x:vec2, y:vec2) { + const ip = this.invPrecision; + const p = this.precision; + x[0] = (Math.round(ip * y[0]) >> 0) * p; + x[1] = (Math.round(ip * y[1]) >> 0) * p; + return x; + } + + public clone (x:vec2) { + const r = MuVec2.alloc(); + return this.assign(r, x); + } + + public alloc () { + return MuVec2.alloc(); + } + + public free (x:vec2) { + return MuVec2.free(x); + } + + public toJSON (x:vec2) { + return [ + this.precision * (Math.round(this.invPrecision * x[0]) >> 0), + this.precision * (Math.round(this.invPrecision * x[1]) >> 0), + ]; + } + + public fromJSON (x:any) { + if (Array.isArray(x) && x.length === 2 && typeof x[0] === 'number' && typeof x[1] === 'number') { + return this.clone((x)); + } + return MuVec2.clone(this.identity); + } + + public equal (x:vec2, y:vec2) { + const sf = this.invPrecision; + return ( + (Math.round(sf * x[0]) >> 0) === (Math.round(sf * y[0]) >> 0) && + (Math.round(sf * x[1]) >> 0) === (Math.round(sf * y[1]) >> 0) + ); + } + + public diff (base:vec2, target:vec2, stream:MuWriteStream) { + const sf = this.invPrecision; + const bx = Math.round(sf * base[0]) >> 0; + const by = Math.round(sf * base[1]) >> 0; + const tx = Math.round(sf * target[0]) >> 0; + const ty = Math.round(sf * target[1]) >> 0; + + if (bx === tx && by === ty) { + return false; + } + const dx = (SCHROEPPEL2 + (tx - bx) ^ SCHROEPPEL2) >>> 0; + const dy = (SCHROEPPEL2 + (ty - by) ^ SCHROEPPEL2) >>> 0; + + const code = + (dx ? 1 : 0) | + (dy ? 2 : 0); + + stream.grow(16); + if (dx) { + writeVarIntWithPrefix2(code, dx, stream); + dy && stream.writeVarint(dy); + } else { + writeVarIntWithPrefix2(code, dy, stream); + } + + return true; + } + + public patch (base:vec2, stream:MuReadStream) { + const prefix = stream.readUint8(); + + let dx = 0; + let dy = 0; + if (prefix & 32) { + dx = readShroeppelWithPrefix2(prefix, stream); + (prefix & 64) && (dy = readShroeppel(stream)); + } else { + dy = readShroeppelWithPrefix2(prefix, stream); + } + + const result = MuVec2.alloc(); + const ip = this.invPrecision; + const p = this.precision; + + const bx = Math.round(ip * base[0]) >> 0; + const by = Math.round(ip * base[1]) >> 0; + + result[0] = p * (bx + dx); + result[1] = p * (by + dy); + + return result; + } +} + +function writeVarIntWithPrefix3 (code:number, x:number, stream:MuWriteStream) { + const x0 = x & 15; + const x1 = x >>> 4; + const prefix = code | (x1 ? 8 : 0); + stream.writeUint8((prefix << 4) | x0); + if (x1) { + stream.writeVarint(x1); + } +} + +function readShroeppelWithPrefix3 (prefix:number, stream:MuReadStream) { + let x = prefix & 15; + if (prefix & 128) { + x += stream.readVarint() << 4; + } + return (SCHROEPPEL2 ^ x) - SCHROEPPEL2 >> 0; +} + +export class MuQuantizedVec3 implements MuSchema { + public invPrecision = 1; + public identity = vec3.create(); + public json:{ + type:'quantized-vec3'; + precision:number; + identity:number[]; + }; + public muData:{ + type:'quantized-vec3'; + precision:number; + identity:number[]; + } = { + type: 'quantized-vec3', + precision: 0, + identity: [0, 0, 0], + }; + public readonly muType = 'quantized-vec3'; + + constructor ( + public precision:number, + identity?:vec3) { + this.invPrecision = 1 / this.precision; + if (identity) { + this.identity[0] = this.precision * (Math.round(this.invPrecision * identity[0]) >> 0); + this.identity[1] = this.precision * (Math.round(this.invPrecision * identity[1]) >> 0); + this.identity[2] = this.precision * (Math.round(this.invPrecision * identity[2]) >> 0); + } + this.json = this.muData = { + type: 'quantized-vec3', + precision: this.precision, + identity: [this.identity[0], this.identity[1], this.identity[2]], + }; + } + + public assign(x:vec3, y:vec3) { + const ip = this.invPrecision; + const p = this.precision; + x[0] = (Math.round(ip * y[0]) >> 0) * p; + x[1] = (Math.round(ip * y[1]) >> 0) * p; + x[2] = (Math.round(ip * y[2]) >> 0) * p; + return x; + } + + public clone (x:vec3) { + const r = MuVec3.alloc(); + return this.assign(r, x); + } + + public alloc () { + return MuVec3.alloc(); + } + + public free (x:vec3) { + return MuVec3.free(x); + } + + public toJSON (x:vec3) { + return [ + this.precision * (Math.round(this.invPrecision * x[0]) >> 0), + this.precision * (Math.round(this.invPrecision * x[1]) >> 0), + this.precision * (Math.round(this.invPrecision * x[2]) >> 0), + ]; + } + + public fromJSON (x:any) { + if (Array.isArray(x) && x.length === 3 && typeof x[0] === 'number' && typeof x[1] === 'number' && typeof x[2] === 'number') { + return this.clone((x)); + } + return MuVec3.clone(this.identity); + } + + public equal (x:vec3, y:vec3) { + const sf = this.invPrecision; + return ( + (Math.round(sf * x[0]) >> 0) === (Math.round(sf * y[0]) >> 0) && + (Math.round(sf * x[1]) >> 0) === (Math.round(sf * y[1]) >> 0) && + (Math.round(sf * x[2]) >> 0) === (Math.round(sf * y[2]) >> 0) + ); + } + + public diff (base:vec3, target:vec3, stream:MuWriteStream) { + const sf = this.invPrecision; + const bx = Math.round(sf * base[0]) >> 0; + const by = Math.round(sf * base[1]) >> 0; + const bz = Math.round(sf * base[2]) >> 0; + const tx = Math.round(sf * target[0]) >> 0; + const ty = Math.round(sf * target[1]) >> 0; + const tz = Math.round(sf * target[2]) >> 0; + + if (bx === tx && by === ty && bz === tz) { + return false; + } + const dx = (SCHROEPPEL2 + (tx - bx) ^ SCHROEPPEL2) >>> 0; + const dy = (SCHROEPPEL2 + (ty - by) ^ SCHROEPPEL2) >>> 0; + const dz = (SCHROEPPEL2 + (tz - bz) ^ SCHROEPPEL2) >>> 0; + + const code = + (dx ? 1 : 0) | + (dy ? 2 : 0) | + (dz ? 4 : 0); + + stream.grow(16); + if (dx) { + writeVarIntWithPrefix3(code, dx, stream); + dy && stream.writeVarint(dy); + dz && stream.writeVarint(dz); + } else if (dy) { + writeVarIntWithPrefix3(code, dy, stream); + dz && stream.writeVarint(dz); + } else { + writeVarIntWithPrefix3(code, dz, stream); + } + + return true; + } + + public patch (base:vec3, stream:MuReadStream) { + const prefix = stream.readUint8(); + + let dx = 0; + let dy = 0; + let dz = 0; + if (prefix & 16) { + dx = readShroeppelWithPrefix3(prefix, stream); + (prefix & 32) && (dy = readShroeppel(stream)); + (prefix & 64) && (dz = readShroeppel(stream)); + } else if (prefix & 32) { + dy = readShroeppelWithPrefix3(prefix, stream); + (prefix & 64) && (dz = readShroeppel(stream)); + } else { + dz = readShroeppelWithPrefix3(prefix, stream); + } + + const result = MuVec3.alloc(); + const ip = this.invPrecision; + const p = this.precision; + + const bx = Math.round(ip * base[0]) >> 0; + const by = Math.round(ip * base[1]) >> 0; + const bz = Math.round(ip * base[2]) >> 0; + + result[0] = p * (bx + dx); + result[1] = p * (by + dy); + result[2] = p * (bz + dz); + + return result; + } +} + +function writeVarIntWithPrefix4 (code:number, x:number, stream:MuWriteStream) { + const x0 = x & 7; + const x1 = x >>> 3; + const prefix = code | (x1 ? 16 : 0); + stream.writeUint8((prefix << 3) | x0); + if (x1) { + stream.writeVarint(x1); + } +} + +function readShroeppelWithPrefix4 (prefix:number, stream:MuReadStream) { + let x = prefix & 7; + if (prefix & 128) { + x += stream.readVarint() << 3; + } + return (SCHROEPPEL2 ^ x) - SCHROEPPEL2 >> 0; +} + +export class MuQuantizedVec4 implements MuSchema { + public invPrecision = 1; + public identity = vec4.create(); + public json:{ + type:'quantized-vec4'; + precision:number; + identity:number[]; + }; + public muData:{ + type:'quantized-vec4'; + precision:number; + identity:number[]; + } = { + type: 'quantized-vec4', + precision: 0, + identity: [0, 0, 0, 0], + }; + public readonly muType = 'quantized-vec4'; + + constructor ( + public precision:number, + identity?:vec4) { + this.invPrecision = 1 / this.precision; + if (identity) { + this.identity[0] = this.precision * (Math.round(this.invPrecision * identity[0]) >> 0); + this.identity[1] = this.precision * (Math.round(this.invPrecision * identity[1]) >> 0); + this.identity[2] = this.precision * (Math.round(this.invPrecision * identity[2]) >> 0); + this.identity[3] = this.precision * (Math.round(this.invPrecision * identity[3]) >> 0); + } + this.json = this.muData = { + type: 'quantized-vec4', + precision: this.precision, + identity: [this.identity[0], this.identity[1], this.identity[2], this.identity[3]], + }; + } + + public assign(x:vec4, y:vec4) { + const ip = this.invPrecision; + const p = this.precision; + x[0] = (Math.round(ip * y[0]) >> 0) * p; + x[1] = (Math.round(ip * y[1]) >> 0) * p; + x[2] = (Math.round(ip * y[2]) >> 0) * p; + x[3] = (Math.round(ip * y[3]) >> 0) * p; + return x; + } + + public clone (x:vec4) { + const r = MuVec4.alloc(); + return this.assign(r, x); + } + + public alloc () { + return MuVec4.alloc(); + } + + public free (x:vec4) { + return MuVec4.free(x); + } + + public toJSON (x:vec4) { + return [ + this.precision * (Math.round(this.invPrecision * x[0]) >> 0), + this.precision * (Math.round(this.invPrecision * x[1]) >> 0), + this.precision * (Math.round(this.invPrecision * x[2]) >> 0), + this.precision * (Math.round(this.invPrecision * x[3]) >> 0), + ]; + } + + public fromJSON (x:any) { + if (Array.isArray(x) && x.length === 4 && typeof x[0] === 'number' && typeof x[1] === 'number' && typeof x[2] === 'number' && typeof x[3] === 'number') { + return this.clone((x)); + } + return MuVec4.clone(this.identity); + } + + public equal (x:vec4, y:vec4) { + const sf = this.invPrecision; + return ( + (Math.round(sf * x[0]) >> 0) === (Math.round(sf * y[0]) >> 0) && + (Math.round(sf * x[1]) >> 0) === (Math.round(sf * y[1]) >> 0) && + (Math.round(sf * x[2]) >> 0) === (Math.round(sf * y[2]) >> 0) && + (Math.round(sf * x[3]) >> 0) === (Math.round(sf * y[3]) >> 0) + ); + } + + public diff (base:vec4, target:vec4, stream:MuWriteStream) { + const sf = this.invPrecision; + const bx = Math.round(sf * base[0]) >> 0; + const by = Math.round(sf * base[1]) >> 0; + const bz = Math.round(sf * base[2]) >> 0; + const bw = Math.round(sf * base[3]) >> 0; + const tx = Math.round(sf * target[0]) >> 0; + const ty = Math.round(sf * target[1]) >> 0; + const tz = Math.round(sf * target[2]) >> 0; + const tw = Math.round(sf * target[3]) >> 0; + + if (bx === tx && by === ty && bz === tz && bw === tw) { + return false; + } + const dx = (SCHROEPPEL2 + (tx - bx) ^ SCHROEPPEL2) >>> 0; + const dy = (SCHROEPPEL2 + (ty - by) ^ SCHROEPPEL2) >>> 0; + const dz = (SCHROEPPEL2 + (tz - bz) ^ SCHROEPPEL2) >>> 0; + const dw = (SCHROEPPEL2 + (tw - bw) ^ SCHROEPPEL2) >>> 0; + + const code = + (dx ? 1 : 0) | + (dy ? 2 : 0) | + (dz ? 4 : 0) | + (dw ? 8 : 0); + + stream.grow(21); + if (dx) { + writeVarIntWithPrefix4(code, dx, stream); + dy && stream.writeVarint(dy); + dz && stream.writeVarint(dz); + dw && stream.writeVarint(dw); + } else if (dy) { + writeVarIntWithPrefix4(code, dy, stream); + dz && stream.writeVarint(dz); + dw && stream.writeVarint(dw); + } else if (dz) { + writeVarIntWithPrefix4(code, dz, stream); + dw && stream.writeVarint(dw); + } else { + writeVarIntWithPrefix4(code, dw, stream); + } + + return true; + } + + public patch (base:vec4, stream:MuReadStream) { + const prefix = stream.readUint8(); + + let dx = 0; + let dy = 0; + let dz = 0; + let dw = 0; + if (prefix & 8) { + dx = readShroeppelWithPrefix4(prefix, stream); + (prefix & 16) && (dy = readShroeppel(stream)); + (prefix & 32) && (dz = readShroeppel(stream)); + (prefix & 64) && (dw = readShroeppel(stream)); + } else if (prefix & 16) { + dy = readShroeppelWithPrefix4(prefix, stream); + (prefix & 32) && (dz = readShroeppel(stream)); + (prefix & 64) && (dw = readShroeppel(stream)); + } else if (prefix & 32) { + dz = readShroeppelWithPrefix4(prefix, stream); + (prefix & 64) && (dw = readShroeppel(stream)); + } else { + dw = readShroeppelWithPrefix4(prefix, stream); + } + + const result = MuVec4.alloc(); + const ip = this.invPrecision; + const p = this.precision; + + const bx = Math.round(ip * base[0]) >> 0; + const by = Math.round(ip * base[1]) >> 0; + const bz = Math.round(ip * base[2]) >> 0; + const bw = Math.round(ip * base[3]) >> 0; + + result[0] = p * (bx + dx); + result[1] = p * (by + dy); + result[2] = p * (bz + dz); + result[3] = p * (bw + dw); + + return result; + } +} diff --git a/src/schema/struct.ts b/src/schema/struct.ts index 483b6038..1ed07bb9 100644 --- a/src/schema/struct.ts +++ b/src/schema/struct.ts @@ -303,6 +303,22 @@ export class MuStruct }> implements M case 'quantized-float': methods.equal.append(`if(((${(type).invPrecision}*a[${pr}])>>0)!==((${(type).invPrecision}*b[${pr}])>>0)){return false}`); break; + case 'quantized-vec2': + case 'quantized-vec3': + case 'quantized-vec4': + const ip = inject(`${(type).invPrecision}`); + const qva = methods.equal.def(`a[${pr}]`); + const qvb = methods.equal.def(`b[${pr}]`); + methods.equal.append(`if(`); + for (let idx = 0; idx < type.identity.length; idx++) { + let code = `(${ip}*${qva}[${idx}]>>0)!==(${ip}*${qvb}[${idx}]>>0)`; + if (idx !== 0) { + code = '||' + code; + } + methods.equal.append(code); + } + methods.equal.append(`){return false}`); + break; default: methods.equal.append(`if(!${typeRefs[i]}.equal(a[${pr}],b[${pr}])){return false}`); } @@ -333,6 +349,11 @@ export class MuStruct }> implements M case 'quantized-float': methods.clone.append(`c[${pr}]=((${(type).invPrecision}*s[${pr}])>>0)*${(type).precision};`); break; + case 'quantized-vec2': + case 'quantized-vec3': + case 'quantized-vec4': + methods.clone.append(`c[${pr}]=${typeRefs[i]}.assign(${typeRefs[i]}.alloc(),s[${pr}]);`); + break; default: methods.clone.append(`c[${pr}]=${typeRefs[i]}.clone(s[${pr}]);`); break; @@ -363,6 +384,15 @@ export class MuStruct }> implements M case 'quantized-float': methods.assign.append(`d[${pr}]=((${(type).invPrecision}*s[${pr}])>>0)*${(type).precision};`); break; + case 'quantized-vec2': + case 'quantized-vec3': + case 'quantized-vec4': + const ip = inject((type).invPrecision); + const p = inject((type).precision); + for (let idx = 0; idx < type.identity.length; idx++) { + methods.assign.append(`d[${pr}][${idx}]=((${ip}*s[${pr}][${idx}])>>0)*${p};`); + } + break; default: methods.assign.append(`d[${pr}]=${typeRefs[i]}.assign(d[${pr}],s[${pr}]);`); } @@ -384,7 +414,8 @@ export class MuStruct }> implements M methods.diff.append(`var head=s.offset;var tr=0;var np=0;s.grow(${baseSize});s.offset+=${trackerBytes};`); propRefs.forEach((pr, i) => { - const muType = types[i].muType; + const type = types[i]; + const muType = type.muType; switch (muType) { case 'boolean': methods.diff.append(`if(b[${pr}]!==t[${pr}]){++np;tr|=${1 << (i & 7)}}`); @@ -412,6 +443,74 @@ export class MuStruct }> implements M const tr = methods.diff.def(`(${(types[i]).invPrecision}*t[${pr}])>>0`); methods.diff.append(`if(${br}!==${tr}){s.writeVarint((0xAAAAAAAA+(${tr}-${br})^0xAAAAAAAA)>>>0);++np;tr|=${1 << (i & 7)};}`); break; + case 'quantized-vec2': + case 'quantized-vec3': + case 'quantized-vec4': + const length = type.identity.length; + const numPrefixBits = length + 1; + const numPackedBits = 8 - numPrefixBits; + + const writeVarintWithPrefix = func('wvwp', ['pp', 'x', 's']); + writeVarintWithPrefix.append( + `var x0=x&${(1 << numPackedBits) - 1};`, + `var x1=x>>>${numPackedBits};`, + `var pr=(x1?${1 << length}:0)|pp;`, + `s.writeUint8((pr<<${numPackedBits})|x0);`, + `if(x1){s.writeVarint(x1)}`, + ); + const wvwp = inject(Function(writeVarintWithPrefix.toString())); + + const sch = inject(0xAAAAAAAA); + const ip = inject((type).invPrecision); + + const qvb = methods.diff.def(`b[${pr}]`); + const qvt = methods.diff.def(`t[${pr}]`); + + const bx = methods.diff.def(`(${ip}*${qvb}[0])>>0`); + const tx = methods.diff.def(`(${ip}*${qvt}[0])>>0`); + const by = methods.diff.def(`(${ip}*${qvb}[1])>>0`); + const ty = methods.diff.def(`(${ip}*${qvt}[1])>>0`); + let bz; + let tz; + let bw; + let tw; + if (length > 2) { + bz = methods.diff.def(`(${ip}*${qvb}[2])>>0`); + tz = methods.diff.def(`(${ip}*${qvt}[2])>>0`); + } + if (length > 3) { + bw = methods.diff.def(`(${ip}*${qvb}[3])>>0`); + tw = methods.diff.def(`(${ip}*${qvt}[3])>>0`); + } + + const dx = methods.diff.def(0); + const dy = methods.diff.def(0); + let dz; + let dw; + (length > 2) && (dz = methods.diff.def(0)); + (length > 3) && (dw = methods.diff.def(0)); + + methods.diff.append(`if(${bx}!==${tx}||${by}!==${ty}`); + (length > 2) && methods.diff.append(`||${bz}!==${tz}`); + (length > 3) && methods.diff.append(`||${bw}!==${tw}`); + methods.diff.append(`){++np;tr|=${1 << (i & 7)};${dx}=(${sch}+(${tx}-${bx})^${sch})>>>0;${dy}=(${sch}+(${ty}-${by})^${sch})>>>0;`); + (length > 2) && methods.diff.append(`${dz}=(${sch}+(${tz}-${bz})^${sch})>>>0;`); + (length > 3) && methods.diff.append(`${dw}=(${sch}+(${tw}-${bw})^${sch})>>>0;`); + + const pp = methods.diff.def(0); + methods.diff.append(`${pp}=(${dx}?1:0)|(${dy}?2:0)`); + (length > 2) && methods.diff.append(`|(${dz}?4:0)`); + (length > 3) && methods.diff.append(`|(${dw}?8:0)`); + methods.diff.append(`;s.grow(21);`); + + methods.diff.append(`if(${dx}){${wvwp}(${pp},${dx},s);${dy}&&s.writeVarint(${dy});`); + (length > 2) && methods.diff.append(`${dz}&&s.writeVarint(${dz});`); + (length > 3) && methods.diff.append(`${dw}&&s.writeVarint(${dw});`); + (length === 3) && methods.diff.append(`}else if(${dy}){${wvwp}(${pp},${dy},s);${dz}&&s.writeVarint(${dz});`); + (length === 4) && methods.diff.append(`}else if(${dy}){${wvwp}(${pp},${dy},s);${dz}&&s.writeVarint(${dz});${dw}&&s.writeVarint(${dw});}else if(${dz}){${wvwp}(${pp},${dz},s);${dw}&&s.writeVarint(${dw});`); + methods.diff.append(`}else{${wvwp}(${pp},${length === 2 ? dy : length === 3 ? dz : dw},s)}`); + methods.diff.append(`}`); + break; default: methods.diff.append(`if(${typeRefs[i]}.diff(b[${pr}],t[${pr}],s)){++np;tr|=${1 << (i & 7)}}`); } @@ -435,34 +534,83 @@ export class MuStruct }> implements M const type = types[i]; const muType = type.muType; - methods.patch.append(`;t[${pr}]=(tr&${1 << (i & 7)})?`); - switch (muType) { - case 'boolean': - methods.patch.append(`!b[${pr}]:b[${pr}];`); - break; - case 'float32': - case 'float64': - case 'int8': - case 'int16': - case 'int32': - case 'uint8': - case 'uint16': - case 'uint32': - case 'utf8': - case 'varint': - methods.patch.append(`s.${muType2ReadMethod[muType]}():b[${pr}];`); - break; - case 'rvarint': - methods.patch.append(`b[${pr}]+((0xAAAAAAAA^s.readVarint())-0xAAAAAAAA>>0):b[${pr}];`); - break; - case 'ascii': - methods.patch.append(`s.readASCII(s.readVarint()):b[${pr}];`); - break; - case 'quantized-float': - methods.patch.append(`(((${(type).invPrecision}*b[${pr}])>>0)+(((0xAAAAAAAA^s.readVarint())-0xAAAAAAAA)>>0))*${(type).precision}:b[${pr}];`); - break; - default: - methods.patch.append(`${typeRefs[i]}.patch(b[${pr}],s):${typeRefs[i]}.clone(b[${pr}]);`); + if (muType === 'quantized-vec2' || muType === 'quantized-vec3' || muType === 'quantized-vec4') { + const length = type.identity.length; + const numPrefixBits = length + 1; + const numPackedBits = 8 - numPrefixBits; + + const readShroeppel = func('rs', ['s']); + readShroeppel.append(`return ((0xAAAAAAAA^s.readVarint())-0xAAAAAAAA)>>0;`); + const rs = inject(Function(readShroeppel.toString())); + + const readShroeppelWithPrefix = func('rswp', ['p', 's']); + readShroeppelWithPrefix.append( + `var x=p&${(1 << numPackedBits) - 1};`, + `if(p&128){x+=s.readVarint()<<${numPackedBits};}`, + `return (0xAAAAAAAA^x)-0xAAAAAAAA>>0;`, + ); + const rswp = inject(Function(readShroeppelWithPrefix.toString())); + + const ip = inject((type).invPrecision); + const p = inject((type).precision); + + const pfx = methods.patch.def(0); + methods.patch.append(`if(tr&${1 << (i & 7)}){${pfx}=s.readUint8();`); + + const dx = methods.patch.def(0); + const dy = methods.patch.def(0); + let dz; + let dw; + (length > 2) && (dz = methods.patch.def(0)); + (length > 3) && (dw = methods.patch.def(0)); + + methods.patch.append(`if(${pfx}&${1 << numPackedBits}){${dx}=${rswp}(${pfx},s);(${pfx}&${1 << (numPackedBits + 1)})&&(${dy}=${rs}(s));`); + (length > 2) && methods.patch.append(`(${pfx}&${1 << (numPackedBits + 2)})&&(${dz}=${rs}(s));`); + (length > 3) && methods.patch.append(`(${pfx}&${1 << (numPackedBits + 3)})&&(${dw}=${rs}(s));`); + (length > 2) && methods.patch.append(`}else if(${pfx}&${1 << (numPackedBits + 1)}){${dy}=${rswp}(${pfx},s);(${pfx}&${1 << (numPackedBits + 2)})&&(${dz}=${rs}(s));`); + (length > 3) && methods.patch.append(`(${pfx}&${1 << (numPackedBits + 3)})&&(${dw}=${rs}(s));}else if(${pfx}&${1 << (numPackedBits + 2)}){${dz}=${rswp}(${pfx},s);(${pfx}&${1 << (numPackedBits + 3)})&&(${dw}=${rs}(s));`); + methods.patch.append(`}else{${length === 2 ? dy : length === 3 ? dz : dw}=${rswp}(${pfx},s)}`); + + const qv = methods.patch.def(null); + methods.patch.append( + `${qv}=${typeRefs[i]}.alloc();`, + `${qv}[0]=${p}*(((${ip}*b[${pr}][0])>>0)+${dx});`, + `${qv}[1]=${p}*(((${ip}*b[${pr}][1])>>0)+${dy});`, + ); + (length > 2) && methods.patch.append(`${qv}[2]=${p}*(((${ip}*b[${pr}][2])>>0)+${dz});`); + (length > 3) && methods.patch.append(`${qv}[3]=${p}*(((${ip}*b[${pr}][3])>>0)+${dw});`); + methods.patch.append(`t[${pr}]=${qv};`); + methods.patch.append(`}else{t[${pr}]=${typeRefs[i]}.clone(b[${pr}])}`); + } else { + methods.patch.append(`;t[${pr}]=(tr&${1 << (i & 7)})?`); + switch (muType) { + case 'boolean': + methods.patch.append(`!b[${pr}]:b[${pr}];`); + break; + case 'float32': + case 'float64': + case 'int8': + case 'int16': + case 'int32': + case 'uint8': + case 'uint16': + case 'uint32': + case 'utf8': + case 'varint': + methods.patch.append(`s.${muType2ReadMethod[muType]}():b[${pr}];`); + break; + case 'rvarint': + methods.patch.append(`b[${pr}]+((0xAAAAAAAA^s.readVarint())-0xAAAAAAAA>>0):b[${pr}];`); + break; + case 'ascii': + methods.patch.append(`s.readASCII(s.readVarint()):b[${pr}];`); + break; + case 'quantized-float': + methods.patch.append(`(((${(type).invPrecision}*b[${pr}])>>0)+(((0xAAAAAAAA^s.readVarint())-0xAAAAAAAA)>>0))*${(type).precision}:b[${pr}];`); + break; + default: + methods.patch.append(`${typeRefs[i]}.patch(b[${pr}],s):${typeRefs[i]}.clone(b[${pr}]);`); + } } }); methods.patch.append(`return t;`); diff --git a/src/schema/test/quantized-vector.ts b/src/schema/test/quantized-vector.ts new file mode 100644 index 00000000..2d5e68be --- /dev/null +++ b/src/schema/test/quantized-vector.ts @@ -0,0 +1,237 @@ +import tape = require('tape'); +import { vec2, vec3, vec4 } from 'gl-matrix'; +import { MuWriteStream, MuReadStream } from '../../stream'; +import { MuQuantizedVec2, MuQuantizedVec3, MuQuantizedVec4 } from '../index'; + +tape('quantized-vec2', function (t) { + function testDiffPatch (x:vec2, y:vec2, schema:MuQuantizedVec2) { + const write = new MuWriteStream(100); + if (!schema.diff(x, y, write)) { + t.equal(write.offset, 0, 'did not write bytes'); + return t.ok(schema.equal(x, y), 'equal'); + } + const read = new MuReadStream(write.buffer.uint8); + const z = schema.patch(x, read); + t.ok(schema.equal(z, y), `diff-patch: ${vec2.str(x)} -> $${vec2.str(y)} got: ${vec2.str(z)} @ precision ${schema.precision}`); + t.equal(read.offset, write.offset, 'used all of stream'); + } + + function testRound (x:vec2, schema:MuQuantizedVec2) { + const z = schema.alloc(); + t.equals(schema.assign(z, x), z, 'assign returns correct value'); + + for (let i = 0; i < 2; ++i) { + t.ok(Math.abs(z[i] - x[i]) <= schema.precision, 'round'); + } + t.ok(schema.equal(x, z), 'equals method works'); + } + + function testPair (x:vec2, y:vec2, schema:MuQuantizedVec2) { + testRound(x, schema); + testRound(y, schema); + testDiffPatch(x, y, schema); + testDiffPatch(y, x, schema); + testDiffPatch(schema.identity, x, schema); + testDiffPatch(schema.identity, y, schema); + testDiffPatch(x, schema.identity, schema); + testDiffPatch(y, schema.identity, schema); + } + + function makeTestSchema (precision:number, identity:vec2) { + const schema = new MuQuantizedVec2(precision, identity); + t.equal(schema.precision, precision, 'precision'); + testRound(identity, schema); + t.ok(schema.equal(identity, schema.identity), 'identity'); + return schema; + } + + function randVec () { + return vec2.fromValues( + 10 * (Math.random() - 0.5), + 10 * (Math.random() - 0.5)); + } + + for (let scale = 0.25; scale <= 2; scale += 0.25) { + const schema0 = makeTestSchema(scale, vec2.create()); + const schema1 = makeTestSchema(scale, vec2.fromValues(1, 1)); + for (let i = 0; i < 40; ++i) { + const x = randVec(); + const y = randVec(); + testPair(x, y, schema0); + testPair(x, y, schema1); + + x[0] = y[0]; + testPair(x, y, schema0); + testPair(x, y, schema1); + + x[1] = y[1]; + testPair(x, y, schema0); + testPair(x, y, schema1); + + x[0] = 10. * Math.random(); + testPair(x, y, schema0); + testPair(x, y, schema1); + } + } + + t.end(); +}); + +tape('quantized-vec3', function (t) { + function testDiffPatch (x:vec3, y:vec3, schema:MuQuantizedVec3) { + const write = new MuWriteStream(100); + if (!schema.diff(x, y, write)) { + t.equal(write.offset, 0, 'did not write bytes'); + return t.ok(schema.equal(x, y), 'equal'); + } + const read = new MuReadStream(write.buffer.uint8); + const z = schema.patch(x, read); + t.ok(schema.equal(z, y), `diff-patch: ${vec3.str(x)} -> $${vec3.str(y)} got: ${vec3.str(z)} @ precision ${schema.precision}`); + t.equal(read.offset, write.offset, 'used all of stream'); + } + + function testRound (x:vec3, schema:MuQuantizedVec3) { + const z = schema.alloc(); + t.equals(schema.assign(z, x), z, 'assign returns correct value'); + + for (let i = 0; i < 3; ++i) { + t.ok(Math.abs(z[i] - x[i]) <= schema.precision, 'round'); + } + t.ok(schema.equal(x, z), 'equals method works'); + } + + function testPair (x:vec3, y:vec3, schema:MuQuantizedVec3) { + testRound(x, schema); + testRound(y, schema); + testDiffPatch(x, y, schema); + testDiffPatch(y, x, schema); + testDiffPatch(schema.identity, x, schema); + testDiffPatch(schema.identity, y, schema); + testDiffPatch(x, schema.identity, schema); + testDiffPatch(y, schema.identity, schema); + } + + function makeTestSchema (precision:number, identity:vec3) { + const schema = new MuQuantizedVec3(precision, identity); + t.equal(schema.precision, precision, 'precision'); + testRound(identity, schema); + t.ok(schema.equal(identity, schema.identity), 'identity'); + return schema; + } + + function randVec () { + return vec3.fromValues( + 10 * (Math.random() - 0.5), + 10 * (Math.random() - 0.5), + 10 * (Math.random() - 0.5)); + } + + for (let scale = 0.25; scale <= 2; scale += 0.25) { + const schema0 = makeTestSchema(scale, vec3.create()); + const schema1 = makeTestSchema(scale, vec3.fromValues(1, 1, 1)); + for (let i = 0; i < 40; ++i) { + const x = randVec(); + const y = randVec(); + testPair(x, y, schema0); + testPair(x, y, schema1); + + x[0] = y[0]; + testPair(x, y, schema0); + testPair(x, y, schema1); + + x[1] = y[1]; + testPair(x, y, schema0); + testPair(x, y, schema1); + + x[0] = 10. * Math.random(); + testPair(x, y, schema0); + testPair(x, y, schema1); + + x[2] = y[2]; + testPair(x, y, schema0); + testPair(x, y, schema1); + + x[1] = 10. * Math.random(); + testPair(x, y, schema0); + testPair(x, y, schema1); + + x[0] = y[0]; + x[1] = 10. * Math.random(); + testPair(x, y, schema0); + testPair(x, y, schema1); + } + } + + t.end(); +}); + +tape('quantized-vec4', function (t) { + function testDiffPatch (x:vec4, y:vec4, schema:MuQuantizedVec4) { + const write = new MuWriteStream(100); + if (!schema.diff(x, y, write)) { + t.equal(write.offset, 0, 'did not write bytes'); + return t.ok(schema.equal(x, y), 'equal'); + } + const read = new MuReadStream(write.buffer.uint8); + const z = schema.patch(x, read); + t.ok(schema.equal(z, y), `diff-patch: ${vec4.str(x)} -> $${vec4.str(y)} got: ${vec4.str(z)} @ precision ${schema.precision}`); + t.equal(read.offset, write.offset, 'used all of stream'); + } + + function testRound (x:vec4, schema:MuQuantizedVec4) { + const z = schema.alloc(); + t.equals(schema.assign(z, x), z, 'assign returns correct value'); + + for (let i = 0; i < 4; ++i) { + t.ok(Math.abs(z[i] - x[i]) <= schema.precision, 'round'); + } + t.ok(schema.equal(x, z), 'equals method works'); + } + + function testPair (x:vec4, y:vec4, schema:MuQuantizedVec4) { + testRound(x, schema); + testRound(y, schema); + testDiffPatch(x, y, schema); + testDiffPatch(y, x, schema); + testDiffPatch(schema.identity, x, schema); + testDiffPatch(schema.identity, y, schema); + testDiffPatch(x, schema.identity, schema); + testDiffPatch(y, schema.identity, schema); + } + + function makeTestSchema (precision:number, identity:vec4) { + const schema = new MuQuantizedVec4(precision, identity); + t.equal(schema.precision, precision, 'precision'); + testRound(identity, schema); + t.ok(schema.equal(identity, schema.identity), 'identity'); + return schema; + } + + function randVec () { + return vec4.fromValues( + 10 * (Math.random() - 0.5), + 10 * (Math.random() - 0.5), + 10 * (Math.random() - 0.5), + 10 * (Math.random() - 0.5)); + } + + for (let scale = 0.25; scale <= 2; scale += 0.25) { + const schema0 = makeTestSchema(scale, vec4.create()); + const schema1 = makeTestSchema(scale, vec4.fromValues(1, 1, 1, 1)); + for (let i = 0; i < 40; ++i) { + for (let k = 0; k << (1 << 4); ++k) { + const nx = randVec(); + const ny = randVec(); + for (let j = 0; j < 4; ++j) { + if (k & (1 << j)) { + ny[j] = nx[j]; + } + } + testPair(nx, ny, schema0); + testPair(nx, ny, schema1); + } + } + } + + t.end(); +});