import type { NonMeshAttributeDataType, NonMeshAttributeDomain, NonMeshAttributeIR, } from "./scene-ir"; export const NON_MESH_BINARY_SCHEMA = 1 as const; export const NON_MESH_MAX_POINTS = 1_000_000; export const NON_MESH_DEFAULT_CHUNK_POINTS = 65_536; export const NON_MESH_MAX_SCENE_BYTES = 256 * 1024 * 1024; export type NonMeshAttributeArray = Float32Array | Int32Array | Uint8Array; export interface NonMeshAttributeSource extends NonMeshAttributeIR { values: NonMeshAttributeArray; } export interface NonMeshGeometrySource { dataId: string; positions: Float32Array; radii?: Float32Array; curveOffsets?: Uint32Array; attributes?: NonMeshAttributeSource[]; } export interface NonMeshAttributeChunk extends NonMeshAttributeIR { storage: "FLOAT32" | "INT32" | "UINT8"; elementOffset: number; elementCount: number; data: ArrayBuffer; } export interface NonMeshGeometryChunk { schemaVersion: typeof NON_MESH_BINARY_SCHEMA; dataId: string; chunkIndex: number; chunkCount: number; pointOffset: number; pointCount: number; totalPointCount: number; byteLength: number; sha256: string; positions: ArrayBuffer; radii?: ArrayBuffer; curveOffsets?: ArrayBuffer; attributes: NonMeshAttributeChunk[]; } export interface NonMeshBinaryBudget { maxPoints: number; maxChunkPoints: number; maxSceneBytes: number; } export interface NonMeshPerformanceMetrics { pointCount: number; chunkCount: number; transferredBytes: number; elapsedMs: number; peakBytes: number; } export interface NonMeshPerformanceGate { status: "READY" | "BLOCKED"; code?: "NON_MESH_DATA_BUDGET_EXCEEDED" | "NON_MESH_PERFORMANCE_BUDGET_EXCEEDED"; metrics: NonMeshPerformanceMetrics; } export interface ReassembledNonMeshAttribute extends NonMeshAttributeIR { storage: NonMeshAttributeChunk["storage"]; values: NonMeshAttributeArray; } export interface ReassembledNonMeshGeometry { dataId: string; positions: Float32Array; radii?: Float32Array; curveOffsets?: Uint32Array; attributes: ReassembledNonMeshAttribute[]; } const defaultBudget: NonMeshBinaryBudget = { maxPoints: NON_MESH_MAX_POINTS, maxChunkPoints: NON_MESH_DEFAULT_CHUNK_POINTS, maxSceneBytes: NON_MESH_MAX_SCENE_BYTES, }; function fail(message: string): never { throw new Error(`NON_MESH_BINARY_INVALID: ${message}`); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function isSafeNonNegativeInteger(value: unknown): value is number { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; } function isArrayBuffer(value: unknown): value is ArrayBuffer { return value instanceof ArrayBuffer; } function storageFor(dataType: NonMeshAttributeDataType): NonMeshAttributeChunk["storage"] { if (dataType === "BOOL" || dataType === "BYTE_COLOR") return "UINT8"; if (dataType === "INT") return "INT32"; return "FLOAT32"; } function bytesPerScalar(storage: NonMeshAttributeChunk["storage"]): number { return storage === "UINT8" ? 1 : 4; } function domainSize(domain: NonMeshAttributeDomain, pointCount: number, curveCount: number): number { if (domain === "POINT") return pointCount; if (domain === "CURVE") return curveCount; return 1; } function exactArrayType(source: NonMeshAttributeSource): boolean { const storage = storageFor(source.dataType); return (storage === "FLOAT32" && source.values instanceof Float32Array) || (storage === "INT32" && source.values instanceof Int32Array) || (storage === "UINT8" && source.values instanceof Uint8Array); } function validateSource(source: NonMeshGeometrySource, budget: NonMeshBinaryBudget): number { if (!source.dataId) fail("dataId is required"); if (!(source.positions instanceof Float32Array) || source.positions.length % 3 !== 0) fail(`${source.dataId} positions must be float32 vec3 values`); const pointCount = source.positions.length / 3; if (pointCount > budget.maxPoints) fail(`${source.dataId} exceeds the ${budget.maxPoints} point budget`); if (source.positions.some((value) => !Number.isFinite(value))) fail(`${source.dataId} positions contain NaN or Infinity`); if (source.radii && (!(source.radii instanceof Float32Array) || source.radii.length !== pointCount || source.radii.some((value) => !Number.isFinite(value) || value < 0))) fail(`${source.dataId} radii do not match the point domain`); let curveCount = 0; if (source.curveOffsets) { const offsets = source.curveOffsets; curveCount = Math.max(0, offsets.length - 1); if (!(offsets instanceof Uint32Array) || offsets.length < 2 || offsets[0] !== 0 || offsets[offsets.length - 1] !== pointCount) fail(`${source.dataId} curve offsets do not cover the point domain`); for (let index = 1; index < offsets.length; index++) if (offsets[index] <= offsets[index - 1]) fail(`${source.dataId} curve offsets must be strictly increasing`); } const names = new Set(); for (const attribute of source.attributes ?? []) { if (!attribute.name || names.has(attribute.name)) fail(`${source.dataId} has a missing or duplicate attribute name`); names.add(attribute.name); if (![1, 2, 3, 4].includes(attribute.components)) fail(`${source.dataId}.${attribute.name} has an invalid component count`); if (!exactArrayType(attribute)) fail(`${source.dataId}.${attribute.name} does not use the required scalar storage`); const elements = domainSize(attribute.domain, pointCount, curveCount); if (attribute.values.length !== elements * attribute.components) fail(`${source.dataId}.${attribute.name} does not match its ${attribute.domain} domain`); if (attribute.values.some((value) => !Number.isFinite(value))) fail(`${source.dataId}.${attribute.name} contains NaN or Infinity`); } return pointCount; } function copyRange(values: NonMeshAttributeArray | Float32Array | Uint32Array, scalarStart: number, scalarCount: number): ArrayBuffer { const bytesPerValue = values.BYTES_PER_ELEMENT; const byteStart = values.byteOffset + scalarStart * bytesPerValue; const output = new Uint8Array(scalarCount * bytesPerValue); output.set(new Uint8Array(values.buffer, byteStart, output.byteLength)); return output.buffer; } function hex(bytes: Uint8Array): string { return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join(""); } async function digestChunk(chunk: Omit): Promise { const subtle = globalThis.crypto?.subtle; if (!subtle) throw new Error("NON_MESH_BINARY_INVALID: SHA-256 is unavailable"); const header = new TextEncoder().encode(JSON.stringify({ schemaVersion: chunk.schemaVersion, dataId: chunk.dataId, chunkIndex: chunk.chunkIndex, chunkCount: chunk.chunkCount, pointOffset: chunk.pointOffset, pointCount: chunk.pointCount, totalPointCount: chunk.totalPointCount, attributes: chunk.attributes.map(({ name, domain, dataType, components, storage, elementOffset, elementCount }) => ({ name, domain, dataType, components, storage, elementOffset, elementCount })), })); const buffers = [chunk.positions, chunk.radii, chunk.curveOffsets, ...chunk.attributes.map((attribute) => attribute.data)].filter((value): value is ArrayBuffer => value instanceof ArrayBuffer); const bytes = new Uint8Array(header.byteLength + buffers.reduce((total, buffer) => total + buffer.byteLength, 0)); bytes.set(header); let offset = header.byteLength; for (const buffer of buffers) { bytes.set(new Uint8Array(buffer), offset); offset += buffer.byteLength; } return hex(new Uint8Array(await subtle.digest("SHA-256", bytes))); } export async function chunkNonMeshGeometry( source: NonMeshGeometrySource, options: Partial = {}, ): Promise { const budget = { ...defaultBudget, ...options }; if (!Number.isSafeInteger(budget.maxPoints) || budget.maxPoints <= 0 || !Number.isSafeInteger(budget.maxChunkPoints) || budget.maxChunkPoints <= 0 || !Number.isSafeInteger(budget.maxSceneBytes) || budget.maxSceneBytes <= 0) fail("binary budget is invalid"); const totalPointCount = validateSource(source, budget); const chunkCount = Math.max(1, Math.ceil(totalPointCount / budget.maxChunkPoints)); const curveCount = Math.max(0, (source.curveOffsets?.length ?? 1) - 1); const chunks: NonMeshGeometryChunk[] = []; let totalBytes = 0; for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++) { const pointOffset = chunkIndex * budget.maxChunkPoints; const pointCount = Math.min(budget.maxChunkPoints, totalPointCount - pointOffset); const positions = copyRange(source.positions, pointOffset * 3, pointCount * 3); const radii = source.radii ? copyRange(source.radii, pointOffset, pointCount) : undefined; const curveOffsets = chunkIndex === 0 && source.curveOffsets ? copyRange(source.curveOffsets, 0, source.curveOffsets.length) : undefined; const attributes = (source.attributes ?? []).flatMap((attribute): NonMeshAttributeChunk[] => { const storage = storageFor(attribute.dataType); const isPoint = attribute.domain === "POINT"; if (!isPoint && chunkIndex !== 0) return []; const elementOffset = isPoint ? pointOffset : 0; const elementCount = isPoint ? pointCount : domainSize(attribute.domain, totalPointCount, curveCount); return [{ name: attribute.name, domain: attribute.domain, dataType: attribute.dataType, components: attribute.components, storage, elementOffset, elementCount, data: copyRange(attribute.values, elementOffset * attribute.components, elementCount * attribute.components), }]; }); const byteLength = positions.byteLength + (radii?.byteLength ?? 0) + (curveOffsets?.byteLength ?? 0) + attributes.reduce((total, attribute) => total + attribute.data.byteLength, 0); totalBytes += byteLength; if (totalBytes > budget.maxSceneBytes) fail(`${source.dataId} exceeds the ${budget.maxSceneBytes} byte scene budget`); const unsigned = { schemaVersion: NON_MESH_BINARY_SCHEMA, dataId: source.dataId, chunkIndex, chunkCount, pointOffset, pointCount, totalPointCount, byteLength, positions, radii, curveOffsets, attributes }; chunks.push({ ...unsigned, sha256: await digestChunk(unsigned) }); } return chunks; } export async function validateNonMeshGeometryChunks( chunks: readonly unknown[], options: Partial = {}, ): Promise { const budget = { ...defaultBudget, ...options }; if (!Array.isArray(chunks) || chunks.length === 0) fail("at least one chunk is required"); if (!Number.isSafeInteger(budget.maxPoints) || budget.maxPoints <= 0 || !Number.isSafeInteger(budget.maxChunkPoints) || budget.maxChunkPoints <= 0 || !Number.isSafeInteger(budget.maxSceneBytes) || budget.maxSceneBytes <= 0) fail("binary budget is invalid"); const first = chunks[0]; if (!isRecord(first) || typeof first.dataId !== "string" || first.dataId.length === 0 || first.dataId.length > 256) fail("chunk dataId is invalid"); const dataId = first.dataId; const totalPointCount = first.totalPointCount; if (!isSafeNonNegativeInteger(totalPointCount) || totalPointCount > budget.maxPoints) fail(`${dataId} total point count exceeds its declared budget`); let expectedPointOffset = 0; let totalBytes = 0; for (const [index, rawChunk] of chunks.entries()) { if (!isRecord(rawChunk)) fail(`${dataId} chunk ${index} is invalid`); const chunk = rawChunk; if (chunk.schemaVersion !== NON_MESH_BINARY_SCHEMA || chunk.dataId !== dataId || !isSafeNonNegativeInteger(chunk.chunkIndex) || chunk.chunkIndex !== index || !isSafeNonNegativeInteger(chunk.chunkCount) || chunk.chunkCount !== chunks.length || chunk.chunkCount === 0 || !isSafeNonNegativeInteger(chunk.totalPointCount) || chunk.totalPointCount !== totalPointCount || !isSafeNonNegativeInteger(chunk.pointOffset) || chunk.pointOffset !== expectedPointOffset) fail(`${dataId} chunk sequence is inconsistent`); if (!isSafeNonNegativeInteger(chunk.pointCount) || chunk.pointCount > budget.maxChunkPoints || chunk.pointOffset > Number.MAX_SAFE_INTEGER - chunk.pointCount || !isArrayBuffer(chunk.positions) || chunk.positions.byteLength !== chunk.pointCount * 3 * 4) fail(`${dataId} chunk ${index} point payload is invalid`); const positions = chunk.positions; const radii = chunk.radii; if (radii !== undefined && (!isArrayBuffer(radii) || radii.byteLength !== chunk.pointCount * 4)) fail(`${dataId} chunk ${index} radius payload is invalid`); const curveOffsets = chunk.curveOffsets; if (curveOffsets !== undefined) { if (!isArrayBuffer(curveOffsets) || index !== 0 || curveOffsets.byteLength < 8 || curveOffsets.byteLength % 4 !== 0) fail(`${dataId} curve offsets are only allowed in the first chunk`); const offsets = new Uint32Array(curveOffsets); if (offsets[0] !== 0 || offsets[offsets.length - 1] !== totalPointCount) fail(`${dataId} curve offsets do not cover the point domain`); for (let offsetIndex = 1; offsetIndex < offsets.length; offsetIndex++) { if (offsets[offsetIndex] <= offsets[offsetIndex - 1] || offsets[offsetIndex] > totalPointCount) fail(`${dataId} curve offsets are invalid`); } } if (!Array.isArray(chunk.attributes)) fail(`${dataId} chunk ${index} attributes must be an array`); const attributes = chunk.attributes; const attributeNames = new Set(); for (const rawAttribute of attributes) { if (!isRecord(rawAttribute)) fail(`${dataId} chunk ${index} has an invalid attribute`); const attribute = rawAttribute; const attributeName = attribute.name; if (typeof attributeName !== "string" || attributeName.length === 0 || attributeNames.has(attributeName)) fail(`${dataId} chunk ${index} has a duplicate or invalid attribute`); attributeNames.add(attributeName); if (!(["POINT", "CURVE", "INSTANCE"].includes(attribute.domain as string)) || !(["FLOAT", "FLOAT2", "FLOAT3", "FLOAT_COLOR", "INT", "BOOL", "BYTE_COLOR"].includes(attribute.dataType as string)) || !(["FLOAT32", "INT32", "UINT8"].includes(attribute.storage as string)) || attribute.storage !== storageFor(attribute.dataType as NonMeshAttributeDataType) || !isSafeNonNegativeInteger(attribute.components) || attribute.components < 1 || attribute.components > 4 || !isSafeNonNegativeInteger(attribute.elementOffset) || !isSafeNonNegativeInteger(attribute.elementCount) || !isArrayBuffer(attribute.data)) fail(`${dataId}.${attributeName} chunk metadata is invalid`); const scalarBytes = bytesPerScalar(attribute.storage as NonMeshAttributeChunk["storage"]); if (attribute.elementCount > Math.floor(Number.MAX_SAFE_INTEGER / attribute.components / scalarBytes)) fail(`${dataId}.${attributeName} chunk scalar count overflows the safe integer range`); const expectedBytes = attribute.elementCount * attribute.components * scalarBytes; if (attribute.data.byteLength !== expectedBytes) fail(`${dataId}.${attributeName} chunk payload is invalid`); if (attribute.domain === "POINT" && (attribute.elementOffset !== chunk.pointOffset || attribute.elementCount !== chunk.pointCount)) fail(`${dataId}.${attributeName} point attribute range is invalid`); if (attribute.domain !== "POINT" && index !== 0) fail(`${dataId}.${attributeName} curve/instance attributes must be transferred once`); } const measuredBytes = positions.byteLength + (radii?.byteLength ?? 0) + (curveOffsets?.byteLength ?? 0) + attributes.reduce((total, attribute) => total + (attribute as Record).data.byteLength, 0); if (!isSafeNonNegativeInteger(chunk.byteLength) || !isSafeNonNegativeInteger(measuredBytes) || chunk.byteLength !== measuredBytes || typeof chunk.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(chunk.sha256) || await digestChunk(chunk as unknown as NonMeshGeometryChunk) !== chunk.sha256) fail(`${dataId} chunk ${index} hash or length is invalid`); totalBytes += measuredBytes; if (!Number.isSafeInteger(totalBytes)) fail(`${dataId} chunk bytes overflow the safe integer range`); expectedPointOffset += chunk.pointCount; } if (expectedPointOffset !== totalPointCount || !Number.isSafeInteger(expectedPointOffset) || totalBytes > budget.maxSceneBytes) fail(`${dataId} chunk set exceeds its declared budget`); } export async function chunkNonMeshScene( sources: readonly NonMeshGeometrySource[], options: Partial = {}, ): Promise { const budget = { ...defaultBudget, ...options }; const ids = new Set(); let sceneBytes = 0; const chunks: NonMeshGeometryChunk[] = []; for (const source of sources) { if (ids.has(source.dataId)) fail(`duplicate dataId ${source.dataId}`); ids.add(source.dataId); const next = await chunkNonMeshGeometry(source, budget); await validateNonMeshGeometryChunks(next, budget); sceneBytes += next.reduce((total, chunk) => total + chunk.byteLength, 0); if (sceneBytes > budget.maxSceneBytes) fail(`scene exceeds the ${budget.maxSceneBytes} byte scene budget`); chunks.push(...next); } return chunks; } export async function benchmarkNonMeshTransfer( source: NonMeshGeometrySource, options: Partial = {}, ): Promise { const start = globalThis.performance?.now() ?? Date.now(); const chunks = await chunkNonMeshGeometry(source, options); await validateNonMeshGeometryChunks(chunks, options); const elapsedMs = (globalThis.performance?.now() ?? Date.now()) - start; const transferredBytes = chunks.reduce((total, chunk) => total + chunk.byteLength, 0); const sourceBytes = source.positions.byteLength + (source.radii?.byteLength ?? 0) + (source.curveOffsets?.byteLength ?? 0) + (source.attributes ?? []).reduce((total, attribute) => total + attribute.values.byteLength, 0); return evaluateNonMeshPerformanceGate({ pointCount: source.positions.length / 3, chunkCount: chunks.length, transferredBytes, elapsedMs, peakBytes: sourceBytes + transferredBytes, }); } export function nonMeshChunkTransferables(chunks: readonly NonMeshGeometryChunk[]): Transferable[] { return chunks.flatMap((chunk) => [chunk.positions, chunk.radii, chunk.curveOffsets, ...chunk.attributes.map((attribute) => attribute.data)].filter((value): value is ArrayBuffer => value instanceof ArrayBuffer)); } function attributeArray(storage: NonMeshAttributeChunk["storage"], length: number): NonMeshAttributeArray { return storage === "FLOAT32" ? new Float32Array(length) : storage === "INT32" ? new Int32Array(length) : new Uint8Array(length); } function chunkAttributeValues(attribute: NonMeshAttributeChunk): NonMeshAttributeArray { return attribute.storage === "FLOAT32" ? new Float32Array(attribute.data) : attribute.storage === "INT32" ? new Int32Array(attribute.data) : new Uint8Array(attribute.data); } /** Reassembles already-transferred WNM chunks while rechecking all cross-chunk invariants synchronously. */ export function reassembleNonMeshGeometry(dataId: string, chunks: readonly NonMeshGeometryChunk[]): ReassembledNonMeshGeometry { const matching = chunks.filter((chunk) => chunk.dataId === dataId).sort((left, right) => left.chunkIndex - right.chunkIndex); if (matching.length === 0) fail(`${dataId} has no geometry chunks`); const first = matching[0]; if (first.totalPointCount > NON_MESH_MAX_POINTS || matching.length !== first.chunkCount) fail(`${dataId} chunk set is incomplete`); const positions = new Float32Array(first.totalPointCount * 3); const radii = matching.some((chunk) => chunk.radii) ? new Float32Array(first.totalPointCount) : undefined; const descriptors = new Map(); let expectedPointAttributes: Set | undefined; let pointOffset = 0; let totalBytes = 0; for (const [chunkIndex, chunk] of matching.entries()) { if (chunk.schemaVersion !== NON_MESH_BINARY_SCHEMA || chunk.chunkIndex !== chunkIndex || chunk.chunkCount !== matching.length || chunk.totalPointCount !== first.totalPointCount || chunk.pointOffset !== pointOffset || chunk.positions.byteLength !== chunk.pointCount * 12) fail(`${dataId} chunk sequence is inconsistent`); positions.set(new Float32Array(chunk.positions), chunk.pointOffset * 3); if (radii) { if (!chunk.radii || chunk.radii.byteLength !== chunk.pointCount * 4) fail(`${dataId} radius chunks are incomplete`); radii.set(new Float32Array(chunk.radii), chunk.pointOffset); } const pointAttributes = new Set(chunk.attributes.filter((attribute) => attribute.domain === "POINT").map((attribute) => `${attribute.domain}\0${attribute.name}`)); if (!expectedPointAttributes) expectedPointAttributes = pointAttributes; else if (pointAttributes.size !== expectedPointAttributes.size || [...expectedPointAttributes].some((key) => !pointAttributes.has(key))) fail(`${dataId} point attribute chunks are incomplete`); for (const attribute of chunk.attributes) { const key = `${attribute.domain}\0${attribute.name}`; let target = descriptors.get(key); const scalarCount = attribute.elementCount * attribute.components; if (!target) { const totalElements = attribute.domain === "POINT" ? first.totalPointCount : attribute.elementCount; target = { name: attribute.name, domain: attribute.domain, dataType: attribute.dataType, components: attribute.components, storage: attribute.storage, values: attributeArray(attribute.storage, totalElements * attribute.components) }; descriptors.set(key, target); } if (target.storage !== attribute.storage || target.dataType !== attribute.dataType || target.components !== attribute.components || attribute.data.byteLength !== scalarCount * bytesPerScalar(attribute.storage)) fail(`${dataId}.${attribute.name} chunks are inconsistent`); target.values.set(chunkAttributeValues(attribute), attribute.elementOffset * attribute.components); } totalBytes += chunk.byteLength; pointOffset += chunk.pointCount; } if (pointOffset !== first.totalPointCount || totalBytes > NON_MESH_MAX_SCENE_BYTES || positions.some((value) => !Number.isFinite(value)) || radii?.some((value) => !Number.isFinite(value) || value < 0)) fail(`${dataId} reassembled geometry is invalid`); const curveOffsets = first.curveOffsets ? new Uint32Array(first.curveOffsets.slice(0)) : undefined; if (curveOffsets) { if (curveOffsets.length < 2 || curveOffsets[0] !== 0 || curveOffsets.at(-1) !== first.totalPointCount) fail(`${dataId} curve offsets do not cover the point domain`); for (let index = 1; index < curveOffsets.length; index++) if (curveOffsets[index] <= curveOffsets[index - 1]) fail(`${dataId} curve offsets are invalid`); } return { dataId, positions, radii, curveOffsets, attributes: [...descriptors.values()] }; } export function evaluateNonMeshPerformanceGate(metrics: NonMeshPerformanceMetrics): NonMeshPerformanceGate { const valid = Object.values(metrics).every((value) => Number.isFinite(value) && value >= 0) && Number.isSafeInteger(metrics.pointCount) && Number.isSafeInteger(metrics.chunkCount) && Number.isSafeInteger(metrics.transferredBytes) && Number.isSafeInteger(metrics.peakBytes); if (!valid || metrics.pointCount > NON_MESH_MAX_POINTS || metrics.transferredBytes > NON_MESH_MAX_SCENE_BYTES) return { status: "BLOCKED", code: "NON_MESH_DATA_BUDGET_EXCEEDED", metrics }; if (metrics.pointCount === NON_MESH_MAX_POINTS && (metrics.elapsedMs > 2_000 || metrics.peakBytes > 512 * 1024 * 1024 || metrics.chunkCount > Math.ceil(NON_MESH_MAX_POINTS / NON_MESH_DEFAULT_CHUNK_POINTS))) { return { status: "BLOCKED", code: "NON_MESH_PERFORMANCE_BUDGET_EXCEEDED", metrics }; } return { status: "READY", metrics }; }