import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates"; import type { ErrorCode } from "./error"; export const PHYSICS_SIMULATION_SCHEMA = 1 as const; export const PHYSICS_CACHE_SCHEMA = 1 as const; export const PHYSICS_CACHE_BLENDER_VERSION_PREFIX = "5.2." as const; export const PHYSICS_SIMULATION_BUDGET = { maxSystems: 4_096, maxDependenciesPerSystem: 1_024, maxSettings: 256, maxSettingsBytes: 64 * 1024, maxFrames: 100_000, maxFrameBytes: 512 * 1024 * 1024, maxCacheBytes: 16 * 1024 * 1024 * 1024, } as const; export const PHYSICS_FAMILIES = [ "RIGID_BODY", "SOFT_BODY", "CLOTH", "FLUID", "DYNAMIC_PAINT", "PARTICLE", "HAIR", ] as const; export type PhysicsFamily = typeof PHYSICS_FAMILIES[number]; export type PhysicsExecutionRequest = "METADATA" | "CACHE_MANIFEST" | "CACHE_PLAYBACK" | "LOCAL_SOLVER" | "SERVER_JOB"; export type PhysicsSettingValue = boolean | number | string | null; export interface PhysicsCacheFrameIR { frame: number; byteOffset: number; byteLength: number; sha256: string; } export interface PhysicsCacheBindingIR { schemaVersion: typeof PHYSICS_CACHE_SCHEMA; cacheKey: string; family: PhysicsFamily; source: "BLENDER_DESKTOP_BAKE" | "BLENDER_SERVER_BAKE"; blenderVersion: string; sourceBlendSha256: string; settingsHash: string; inputHash: string; cacheSha256: string; frameStart: number; frameEnd: number; byteLength: number; frames: PhysicsCacheFrameIR[]; status: "COMPLETE" | "PARTIAL"; } export interface PhysicsSystemIR { id: string; family: PhysicsFamily; ownerObjectId: string; settingsHash: string; settings: Record; dependencyIds: string[]; cache?: PhysicsCacheBindingIR; } export interface PhysicsSimulationManifestIR { schemaVersion: typeof PHYSICS_SIMULATION_SCHEMA; systems: PhysicsSystemIR[]; } export interface PhysicsFamilyCapabilityIR { family: PhysicsFamily; metadata: "LOCAL_BOUNDED"; cacheManifest: "LOCAL_BOUNDED"; cachePlayback: "BLOCKED"; localSolver: "READY" | "BLOCKED"; solverProbe: PhysicsSolverProbeStatus; unsupportedRoute: "DESKTOP_SERVER_BAKE"; serverJob: "BLOCKED"; } export type PhysicsSolverProbeStatus = | "READY" | "EXPORT_UNAVAILABLE" | "INITIALIZATION_FAILED" | "THREADS_UNAVAILABLE" | "MEMORY_UNAVAILABLE" | "INVALID_RESULT"; export interface PhysicsSolverProbeEnvironmentIR { threadMode: "SINGLE" | "PTHREAD"; memoryLimitBytes: number; } export interface PhysicsSolverInitializationIR { initialized: boolean; requiredThreadMode: "SINGLE" | "PTHREAD"; requiredMemoryBytes: number; } export interface PhysicsSolverRuntimeProbe { hasFamilyExport(family: PhysicsFamily): boolean; initializeFamily(family: PhysicsFamily): PhysicsSolverInitializationIR | Promise; } export interface PhysicsExecutionRouteIR { family: PhysicsFamily; mode: "LOCAL_SOLVER" | "DESKTOP_SERVER_BAKE"; probe: PhysicsSolverProbeStatus; } export interface BrowserTransformCacheObjectIR { objectId: string; translation: [number, number, number]; rotationQuaternion: [number, number, number, number]; scale: [number, number, number]; } export interface BrowserTransformCacheFrameIR { schemaVersion: 1; frame: number; objects: BrowserTransformCacheObjectIR[]; } export class PhysicsSimulationValidationError extends Error { readonly code: ErrorCode; constructor(code: ErrorCode, message: string) { super(`${code}: ${message}`); this.name = "PhysicsSimulationValidationError"; this.code = code; } } const SHA256 = /^[a-f0-9]{64}$/; const CACHE_KEY = /^[A-Za-z0-9._:-]{1,256}$/; function record(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function text(value: unknown, name: string, prefix?: string): string { if (typeof value !== "string" || value.length === 0 || value.length > 256 || (prefix && !value.startsWith(prefix))) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `${name} is invalid`); } return value; } function digest(value: unknown, name: string): string { if (typeof value !== "string" || !SHA256.test(value)) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `${name} must be a lowercase SHA-256 digest`); } return value; } function integer(value: unknown, name: string, minimum = 0, maximum = Number.MAX_SAFE_INTEGER): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `${name} is outside its integer range`); } return value; } function exactKeys(value: Record, allowed: readonly string[], name: string): void { const allowedSet = new Set(allowed); if (Object.keys(value).some((key) => !allowedSet.has(key))) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `${name} contains undeclared fields`); } } function frame(value: unknown, name: string): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < -1_000_000 || value > 1_000_000) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `${name} is outside the supported frame range`); } return value; } function parseSettings(value: unknown, systemIndex: number): Record { if (!record(value)) throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `systems[${systemIndex}].settings must be an object`); const entries = Object.entries(value); if (entries.length > PHYSICS_SIMULATION_BUDGET.maxSettings) { throw new PhysicsSimulationValidationError("PHYSICS_BUDGET_EXCEEDED", `systems[${systemIndex}].settings exceeds the entry budget`); } const settings: Record = {}; for (const [name, item] of entries) { if (name.length === 0 || name.length > 128 || !/^[A-Za-z0-9_.:-]+$/.test(name) || !(item === null || typeof item === "boolean" || typeof item === "string" || (typeof item === "number" && Number.isFinite(item)))) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `systems[${systemIndex}].settings.${name} is invalid`); } if (typeof item === "string" && item.length > 1_024) { throw new PhysicsSimulationValidationError("PHYSICS_BUDGET_EXCEEDED", `systems[${systemIndex}].settings.${name} exceeds the string budget`); } settings[name] = item; } if (new TextEncoder().encode(JSON.stringify(settings)).byteLength > PHYSICS_SIMULATION_BUDGET.maxSettingsBytes) { throw new PhysicsSimulationValidationError("PHYSICS_BUDGET_EXCEEDED", `systems[${systemIndex}].settings exceeds the byte budget`); } return settings; } function parseCache( value: unknown, settingsHash: string, family: PhysicsFamily, systemIndex: number, ): PhysicsCacheBindingIR | undefined { if (value === undefined) return undefined; if (!record(value)) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `systems[${systemIndex}].cache is invalid`); } exactKeys(value, [ "schemaVersion", "cacheKey", "family", "source", "blenderVersion", "sourceBlendSha256", "settingsHash", "inputHash", "cacheSha256", "frameStart", "frameEnd", "byteLength", "frames", "status", ], `systems[${systemIndex}].cache`); if (value.schemaVersion !== PHYSICS_CACHE_SCHEMA) { throw new PhysicsSimulationValidationError("PROTOCOL_MISMATCH", `systems[${systemIndex}].cache has an unsupported schema`); } if (value.family !== family || (value.source !== "BLENDER_DESKTOP_BAKE" && value.source !== "BLENDER_SERVER_BAKE") || !CACHE_KEY.test(String(value.cacheKey ?? "")) || (value.status !== "COMPLETE" && value.status !== "PARTIAL")) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `systems[${systemIndex}].cache is invalid`); } const blenderVersion = text(value.blenderVersion, `systems[${systemIndex}].cache.blenderVersion`); if (!blenderVersion.startsWith(PHYSICS_CACHE_BLENDER_VERSION_PREFIX)) { throw new PhysicsSimulationValidationError("PROTOCOL_MISMATCH", `Physics cache requires Blender ${PHYSICS_CACHE_BLENDER_VERSION_PREFIX}x`); } const frameStart = frame(value.frameStart, `systems[${systemIndex}].cache.frameStart`); const frameEnd = frame(value.frameEnd, `systems[${systemIndex}].cache.frameEnd`); const byteLength = integer(value.byteLength, `systems[${systemIndex}].cache.byteLength`, 1); if (byteLength > PHYSICS_SIMULATION_BUDGET.maxCacheBytes) { throw new PhysicsSimulationValidationError("PHYSICS_BUDGET_EXCEEDED", `systems[${systemIndex}].cache exceeds the byte budget`); } if (frameEnd < frameStart || frameEnd - frameStart + 1 > PHYSICS_SIMULATION_BUDGET.maxFrames || !Array.isArray(value.frames)) { throw new PhysicsSimulationValidationError("PHYSICS_BUDGET_EXCEEDED", `systems[${systemIndex}].cache frame range exceeds the budget`); } if (value.frames.length === 0 || value.frames.length > PHYSICS_SIMULATION_BUDGET.maxFrames) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `systems[${systemIndex}].cache frames are invalid`); } let nextOffset = 0; const frames = value.frames.map((item, frameIndex): PhysicsCacheFrameIR => { if (!record(item)) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `systems[${systemIndex}].cache.frames[${frameIndex}] is invalid`); } exactKeys(item, ["frame", "byteOffset", "byteLength", "sha256"], `systems[${systemIndex}].cache.frames[${frameIndex}]`); const frameNumber = frame(item.frame, `systems[${systemIndex}].cache.frames[${frameIndex}].frame`); const byteOffset = integer(item.byteOffset, `systems[${systemIndex}].cache.frames[${frameIndex}].byteOffset`); const frameByteLength = integer(item.byteLength, `systems[${systemIndex}].cache.frames[${frameIndex}].byteLength`, 1); if (frameByteLength > PHYSICS_SIMULATION_BUDGET.maxFrameBytes) { throw new PhysicsSimulationValidationError("PHYSICS_BUDGET_EXCEEDED", `systems[${systemIndex}].cache.frames[${frameIndex}] exceeds the byte budget`); } if (frameNumber < frameStart || frameNumber > frameEnd || byteOffset !== nextOffset || byteOffset > byteLength - frameByteLength) { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `systems[${systemIndex}].cache.frames[${frameIndex}] is not ordered or contiguous`); } nextOffset += frameByteLength; return { frame: frameNumber, byteOffset, byteLength: frameByteLength, sha256: digest(item.sha256, `systems[${systemIndex}].cache.frames[${frameIndex}].sha256`), }; }); if (nextOffset !== byteLength) { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `systems[${systemIndex}].cache frame ranges do not cover the payload`); } if (frames.some((item, index) => index > 0 && item.frame <= frames[index - 1].frame)) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `systems[${systemIndex}].cache frames must be unique, ordered and in range`); } if (value.status === "COMPLETE" && (frames.length !== frameEnd - frameStart + 1 || frames.some((item, index) => item.frame !== frameStart + index))) { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `systems[${systemIndex}] declares an incomplete cache as COMPLETE`); } const cacheSettingsHash = digest(value.settingsHash, `systems[${systemIndex}].cache.settingsHash`); if (cacheSettingsHash !== settingsHash) { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `systems[${systemIndex}] cache settings do not match the current system`); } return { schemaVersion: PHYSICS_CACHE_SCHEMA, cacheKey: value.cacheKey as string, family, source: value.source, blenderVersion, sourceBlendSha256: digest(value.sourceBlendSha256, `systems[${systemIndex}].cache.sourceBlendSha256`), settingsHash: cacheSettingsHash, inputHash: digest(value.inputHash, `systems[${systemIndex}].cache.inputHash`), cacheSha256: digest(value.cacheSha256, `systems[${systemIndex}].cache.cacheSha256`), frameStart, frameEnd, byteLength, frames, status: value.status, }; } export function parsePhysicsSimulationManifest(value: unknown): PhysicsSimulationManifestIR { if (!record(value) || value.schemaVersion !== PHYSICS_SIMULATION_SCHEMA || !Array.isArray(value.systems)) { throw new PhysicsSimulationValidationError("PROTOCOL_MISMATCH", "Unsupported PhysicsSimulation manifest schema"); } if (value.systems.length > PHYSICS_SIMULATION_BUDGET.maxSystems) { throw new PhysicsSimulationValidationError("PHYSICS_BUDGET_EXCEEDED", "Physics system count exceeds the budget"); } const ids = new Set(); const systems = value.systems.map((item, index): PhysicsSystemIR => { if (!record(item) || !PHYSICS_FAMILIES.includes(item.family as PhysicsFamily) || !Array.isArray(item.dependencyIds)) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `systems[${index}] is invalid`); } const id = text(item.id, `systems[${index}].id`, "physics:"); if (ids.has(id)) throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `Duplicate physics system ${id}`); ids.add(id); if (item.dependencyIds.length > PHYSICS_SIMULATION_BUDGET.maxDependenciesPerSystem) { throw new PhysicsSimulationValidationError("PHYSICS_BUDGET_EXCEEDED", `systems[${index}].dependencyIds exceeds the budget`); } const dependencyIds = item.dependencyIds.map((dependency, dependencyIndex) => text(dependency, `systems[${index}].dependencyIds[${dependencyIndex}]`)); if (new Set(dependencyIds).size !== dependencyIds.length) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `systems[${index}] contains duplicate dependencies`); } const family = item.family as PhysicsFamily; const settingsHash = digest(item.settingsHash, `systems[${index}].settingsHash`); return { id, family, ownerObjectId: text(item.ownerObjectId, `systems[${index}].ownerObjectId`, "object:"), settingsHash, settings: parseSettings(item.settings, index), dependencyIds, cache: parseCache(item.cache, settingsHash, family, index), }; }); const byId = new Map(systems.map((system) => [system.id, system])); const active = new Set(); const complete = new Set(); const visit = (id: string): void => { if (active.has(id)) throw new PhysicsSimulationValidationError("PHYSICS_DEPENDENCY_CYCLE", `Physics dependency cycle includes ${id}`); if (complete.has(id)) return; active.add(id); for (const dependency of byId.get(id)?.dependencyIds ?? []) if (byId.has(dependency)) visit(dependency); active.delete(id); complete.add(id); }; for (const system of systems) visit(system.id); return { schemaVersion: PHYSICS_SIMULATION_SCHEMA, systems }; } export function physicsCapabilityInventory(): PhysicsFamilyCapabilityIR[] { return PHYSICS_FAMILIES.map((family) => ({ family, metadata: "LOCAL_BOUNDED", cacheManifest: "LOCAL_BOUNDED", cachePlayback: "BLOCKED", localSolver: "BLOCKED", solverProbe: "EXPORT_UNAVAILABLE", unsupportedRoute: "DESKTOP_SERVER_BAKE", serverJob: "BLOCKED", })); } function solverCapability(family: PhysicsFamily, status: PhysicsSolverProbeStatus): PhysicsFamilyCapabilityIR { return { family, metadata: "LOCAL_BOUNDED", cacheManifest: "LOCAL_BOUNDED", cachePlayback: "BLOCKED", localSolver: status === "READY" ? "READY" : "BLOCKED", solverProbe: status, unsupportedRoute: "DESKTOP_SERVER_BAKE", serverJob: "BLOCKED", }; } function validProbeEnvironment(value: PhysicsSolverProbeEnvironmentIR): boolean { return (value.threadMode === "SINGLE" || value.threadMode === "PTHREAD") && Number.isSafeInteger(value.memoryLimitBytes) && value.memoryLimitBytes > 0 && value.memoryLimitBytes <= 2_147_483_648; } function validInitialization(value: unknown): value is PhysicsSolverInitializationIR { return record(value) && typeof value.initialized === "boolean" && (value.requiredThreadMode === "SINGLE" || value.requiredThreadMode === "PTHREAD") && typeof value.requiredMemoryBytes === "number" && Number.isSafeInteger(value.requiredMemoryBytes) && value.requiredMemoryBytes > 0 && value.requiredMemoryBytes <= 2_147_483_648; } export async function probePhysicsSolverCapabilities( runtime: PhysicsSolverRuntimeProbe | undefined, environment: PhysicsSolverProbeEnvironmentIR, ): Promise { if (!validProbeEnvironment(environment)) { throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", "Physics solver probe environment is invalid"); } const capabilities: PhysicsFamilyCapabilityIR[] = []; for (const family of PHYSICS_FAMILIES) { if (!runtime) { capabilities.push(solverCapability(family, "EXPORT_UNAVAILABLE")); continue; } let hasExport = false; try { hasExport = runtime.hasFamilyExport(family) === true; } catch { /* A failed symbol lookup is unavailable, not implicit support. */ } if (!hasExport) { capabilities.push(solverCapability(family, "EXPORT_UNAVAILABLE")); continue; } let initialized: unknown; try { initialized = await runtime.initializeFamily(family); } catch { capabilities.push(solverCapability(family, "INITIALIZATION_FAILED")); continue; } if (!validInitialization(initialized)) { capabilities.push(solverCapability(family, "INVALID_RESULT")); continue; } if (!initialized.initialized) { capabilities.push(solverCapability(family, "INITIALIZATION_FAILED")); continue; } if (initialized.requiredThreadMode === "PTHREAD" && environment.threadMode !== "PTHREAD") { capabilities.push(solverCapability(family, "THREADS_UNAVAILABLE")); continue; } if (initialized.requiredMemoryBytes > environment.memoryLimitBytes) { capabilities.push(solverCapability(family, "MEMORY_UNAVAILABLE")); continue; } capabilities.push(solverCapability(family, "READY")); } return capabilities; } export function selectPhysicsExecutionRoute( family: PhysicsFamily, capabilities: readonly PhysicsFamilyCapabilityIR[], ): PhysicsExecutionRouteIR { const capability = capabilities.find((entry) => entry.family === family); if (!capability || capability.localSolver !== "READY" || capability.solverProbe !== "READY") { return { family, mode: "DESKTOP_SERVER_BAKE", probe: capability?.solverProbe ?? "EXPORT_UNAVAILABLE" }; } return { family, mode: "LOCAL_SOLVER", probe: "READY" }; } export function gatePhysicsExecution( family: PhysicsFamily, request: PhysicsExecutionRequest, capabilities: readonly PhysicsFamilyCapabilityIR[] = physicsCapabilityInventory(), ): CapabilityGateResult { if (request === "METADATA" || request === "CACHE_MANIFEST") return readyGate("N-018", `${family}_${request}`); const route = selectPhysicsExecutionRoute(family, capabilities); if (request === "LOCAL_SOLVER" && route.mode === "LOCAL_SOLVER") return readyGate("N-018", `${family}_${request}`); const issue = request === "CACHE_PLAYBACK" ? capabilityIssue("PHYSICS_CACHE_PLAYBACK_UNAVAILABLE", `${family} cache playback is not connected to frame evaluation`) : request === "LOCAL_SOLVER" ? capabilityIssue("PHYSICS_SOLVER_UNAVAILABLE", `${family} local WASM solver probe is ${route.probe}; use a verified desktop/server bake`) : capabilityIssue("PHYSICS_SERVER_UNAVAILABLE", `${family} server job execution is not configured`); return blockedGate("N-018", `${family}_${request}`, [issue]); } export function selectPhysicsCacheFrame(system: PhysicsSystemIR, requestedFrame: number): { cacheKey: string; frame: number } { const cache = system.cache; if (!Number.isSafeInteger(requestedFrame) || !cache || !cache.frames.some((item) => item.frame === requestedFrame)) { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `Physics cache has no verified frame ${requestedFrame}`); } return { cacheKey: cache.cacheKey, frame: requestedFrame }; } async function sha256(value: ArrayBuffer): Promise { const hash = await crypto.subtle.digest("SHA-256", value); return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join(""); } export async function verifyPhysicsCachePayload( system: PhysicsSystemIR, sourceBlend: ArrayBuffer, payload: ArrayBuffer, ): Promise { const cache = parseCache(system.cache, system.settingsHash, system.family, 0); if (!cache) throw new PhysicsSimulationValidationError("PHYSICS_MANIFEST_INVALID", `Physics system ${system.id} has no cache`); if (!(sourceBlend instanceof ArrayBuffer) || await sha256(sourceBlend) !== cache.sourceBlendSha256) { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_SOURCE_MISMATCH", `Physics ${cache.family} cache source does not match the current blend`); } if (!(payload instanceof ArrayBuffer) || payload.byteLength !== cache.byteLength || await sha256(payload) !== cache.cacheSha256) { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_HASH_MISMATCH", `Physics ${cache.family} cache payload failed SHA-256 verification`); } for (const cacheFrame of cache.frames) { const bytes = payload.slice(cacheFrame.byteOffset, cacheFrame.byteOffset + cacheFrame.byteLength); if (await sha256(bytes) !== cacheFrame.sha256) { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_HASH_MISMATCH", `Physics ${cache.family} frame ${cacheFrame.frame} failed SHA-256 verification`); } } return cache; } const BROWSER_TRANSFORM_CACHE_MAGIC = 0x31465442; // BTF1 const BROWSER_TRANSFORM_CACHE_HEADER_BYTES = 16; const BROWSER_TRANSFORM_CACHE_OBJECT_BYTES = 72; export function decodeBrowserTransformCacheFrame(value: ArrayBuffer): BrowserTransformCacheFrameIR { if (!(value instanceof ArrayBuffer) || value.byteLength < BROWSER_TRANSFORM_CACHE_HEADER_BYTES) { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", "Browser transform cache frame is truncated"); } const view = new DataView(value); if (view.getUint32(0, true) !== BROWSER_TRANSFORM_CACHE_MAGIC || view.getUint16(4, true) !== 1 || view.getUint16(6, true) !== BROWSER_TRANSFORM_CACHE_HEADER_BYTES) { throw new PhysicsSimulationValidationError("PROTOCOL_MISMATCH", "Unsupported browser transform cache frame schema"); } const frameNumber = view.getInt32(8, true); const count = view.getUint32(12, true); if (frameNumber < -1_000_000 || frameNumber > 1_000_000 || count > PHYSICS_SIMULATION_BUDGET.maxSystems || value.byteLength !== BROWSER_TRANSFORM_CACHE_HEADER_BYTES + count * BROWSER_TRANSFORM_CACHE_OBJECT_BYTES) { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", "Browser transform cache frame length or count is invalid"); } const decoder = new TextDecoder("utf-8", { fatal: true }); const objects: BrowserTransformCacheObjectIR[] = []; const ids = new Set(); for (let index = 0; index < count; index += 1) { const offset = BROWSER_TRANSFORM_CACHE_HEADER_BYTES + index * BROWSER_TRANSFORM_CACHE_OBJECT_BYTES; const idLength = view.getUint8(offset); if (idLength === 0 || idLength > 31) throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `Browser transform cache object ${index} has an invalid ID length`); let objectId: string; try { objectId = decoder.decode(new Uint8Array(value, offset + 1, idLength)); } catch { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `Browser transform cache object ${index} has invalid UTF-8`); } if (!objectId.startsWith("object:") || ids.has(objectId)) throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `Browser transform cache object ${index} has an invalid or duplicate ID`); ids.add(objectId); const numbers = Array.from({ length: 10 }, (_, component) => view.getFloat32(offset + 32 + component * 4, true)); if (numbers.some((component) => !Number.isFinite(component))) throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `Browser transform cache object ${index} has non-finite transforms`); const quaternionLength = Math.hypot(numbers[3], numbers[4], numbers[5], numbers[6]); if (Math.abs(quaternionLength - 1) > 1e-3 || numbers.slice(7).some((component) => component === 0)) throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `Browser transform cache object ${index} has an invalid rotation or scale`); objects.push({ objectId, translation: numbers.slice(0, 3) as [number, number, number], rotationQuaternion: numbers.slice(3, 7) as [number, number, number, number], scale: numbers.slice(7, 10) as [number, number, number], }); } return { schemaVersion: 1, frame: frameNumber, objects }; }